Home | History | Annotate | Download | only in editing
      1 /*
      2  * Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
      3  * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
      4  *
      5  * Redistribution and use in source and binary forms, with or without
      6  * modification, are permitted provided that the following conditions
      7  * are met:
      8  * 1. Redistributions of source code must retain the above copyright
      9  *    notice, this list of conditions and the following disclaimer.
     10  * 2. Redistributions in binary form must reproduce the above copyright
     11  *    notice, this list of conditions and the following disclaimer in the
     12  *    documentation and/or other materials provided with the distribution.
     13  *
     14  * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
     15  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
     16  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
     17  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
     18  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
     19  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
     20  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
     21  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
     22  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     23  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     24  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     25  */
     26 
     27 #include "config.h"
     28 #include "SpellingCorrectionController.h"
     29 
     30 #include "DocumentMarkerController.h"
     31 #include "EditCommand.h"
     32 #include "EditorClient.h"
     33 #include "Frame.h"
     34 #include "FrameView.h"
     35 #include "SpellingCorrectionCommand.h"
     36 #include "TextCheckerClient.h"
     37 #include "TextCheckingHelper.h"
     38 #include "TextIterator.h"
     39 #include "htmlediting.h"
     40 #include "markup.h"
     41 #include "visible_units.h"
     42 
     43 
     44 namespace WebCore {
     45 
     46 using namespace std;
     47 using namespace WTF;
     48 
     49 #if SUPPORT_AUTOCORRECTION_PANEL
     50 
     51 static const Vector<DocumentMarker::MarkerType>& markerTypesForAutocorrection()
     52 {
     53     DEFINE_STATIC_LOCAL(Vector<DocumentMarker::MarkerType>, markerTypesForAutoCorrection, ());
     54     if (markerTypesForAutoCorrection.isEmpty()) {
     55         markerTypesForAutoCorrection.append(DocumentMarker::Replacement);
     56         markerTypesForAutoCorrection.append(DocumentMarker::CorrectionIndicator);
     57         markerTypesForAutoCorrection.append(DocumentMarker::SpellCheckingExemption);
     58         markerTypesForAutoCorrection.append(DocumentMarker::Autocorrected);
     59     }
     60     return markerTypesForAutoCorrection;
     61 }
     62 
     63 static const Vector<DocumentMarker::MarkerType>& markerTypesForReplacement()
     64 {
     65     DEFINE_STATIC_LOCAL(Vector<DocumentMarker::MarkerType>, markerTypesForReplacement, ());
     66     if (markerTypesForReplacement.isEmpty()) {
     67         markerTypesForReplacement.append(DocumentMarker::Replacement);
     68         markerTypesForReplacement.append(DocumentMarker::SpellCheckingExemption);
     69     }
     70     return markerTypesForReplacement;
     71 }
     72 
     73 static bool markersHaveIdenticalDescription(const Vector<DocumentMarker>& markers)
     74 {
     75     if (markers.isEmpty())
     76         return true;
     77 
     78     const String& description = markers[0].description;
     79     for (size_t i = 1; i < markers.size(); ++i) {
     80         if (description != markers[i].description)
     81             return false;
     82     }
     83     return true;
     84 }
     85 
     86 SpellingCorrectionController::SpellingCorrectionController(Frame* frame)
     87     : m_frame(frame)
     88     , m_correctionPanelTimer(this, &SpellingCorrectionController::correctionPanelTimerFired)
     89 {
     90 }
     91 
     92 SpellingCorrectionController::~SpellingCorrectionController()
     93 {
     94     dismiss(ReasonForDismissingCorrectionPanelIgnored);
     95 }
     96 
     97 void SpellingCorrectionController::startCorrectionPanelTimer(CorrectionPanelInfo::PanelType type)
     98 {
     99     const double correctionPanelTimerInterval = 0.3;
    100     if (!isAutomaticSpellingCorrectionEnabled())
    101         return;
    102 
    103     // If type is PanelTypeReversion, then the new range has been set. So we shouldn't clear it.
    104     if (type == CorrectionPanelInfo::PanelTypeCorrection)
    105         m_correctionPanelInfo.rangeToBeReplaced.clear();
    106     m_correctionPanelInfo.panelType = type;
    107     m_correctionPanelTimer.startOneShot(correctionPanelTimerInterval);
    108 }
    109 
    110 void SpellingCorrectionController::stopCorrectionPanelTimer()
    111 {
    112     m_correctionPanelTimer.stop();
    113     m_correctionPanelInfo.rangeToBeReplaced.clear();
    114 }
    115 
    116 void SpellingCorrectionController::stopPendingCorrection(const VisibleSelection& oldSelection)
    117 {
    118     // Make sure there's no pending autocorrection before we call markMisspellingsAndBadGrammar() below.
    119     VisibleSelection currentSelection(m_frame->selection()->selection());
    120     if (currentSelection == oldSelection)
    121         return;
    122 
    123     stopCorrectionPanelTimer();
    124     dismiss(ReasonForDismissingCorrectionPanelIgnored);
    125 }
    126 
    127 void SpellingCorrectionController::applyPendingCorrection(const VisibleSelection& selectionAfterTyping)
    128 {
    129     // Apply pending autocorrection before next round of spell checking.
    130     bool doApplyCorrection = true;
    131     VisiblePosition startOfSelection = selectionAfterTyping.visibleStart();
    132     VisibleSelection currentWord = VisibleSelection(startOfWord(startOfSelection, LeftWordIfOnBoundary), endOfWord(startOfSelection, RightWordIfOnBoundary));
    133     if (currentWord.visibleEnd() == startOfSelection) {
    134         String wordText = plainText(currentWord.toNormalizedRange().get());
    135         if (wordText.length() > 0 && isAmbiguousBoundaryCharacter(wordText[wordText.length() - 1]))
    136             doApplyCorrection = false;
    137     }
    138     if (doApplyCorrection)
    139         handleCorrectionPanelResult(dismissSoon(ReasonForDismissingCorrectionPanelAccepted));
    140     else
    141         m_correctionPanelInfo.rangeToBeReplaced.clear();
    142 }
    143 
    144 bool SpellingCorrectionController::hasPendingCorrection() const
    145 {
    146     return m_correctionPanelInfo.rangeToBeReplaced;
    147 }
    148 
    149 bool SpellingCorrectionController::isSpellingMarkerAllowed(PassRefPtr<Range> misspellingRange) const
    150 {
    151     return !m_frame->document()->markers()->hasMarkers(misspellingRange.get(), DocumentMarker::SpellCheckingExemption);
    152 }
    153 
    154 void SpellingCorrectionController::show(PassRefPtr<Range> rangeToReplace, const String& replacement)
    155 {
    156     FloatRect boundingBox = windowRectForRange(rangeToReplace.get());
    157     if (boundingBox.isEmpty())
    158         return;
    159     m_correctionPanelInfo.replacedString = plainText(rangeToReplace.get());
    160     m_correctionPanelInfo.rangeToBeReplaced = rangeToReplace;
    161     m_correctionPanelInfo.replacementString = replacement;
    162     m_correctionPanelInfo.isActive = true;
    163     client()->showCorrectionPanel(m_correctionPanelInfo.panelType, boundingBox, m_correctionPanelInfo.replacedString, replacement, Vector<String>());
    164 }
    165 
    166 void SpellingCorrectionController::handleCancelOperation()
    167 {
    168     if (!m_correctionPanelInfo.isActive)
    169         return;
    170     m_correctionPanelInfo.isActive = false;
    171     dismiss(ReasonForDismissingCorrectionPanelCancelled);
    172 }
    173 
    174 void SpellingCorrectionController::dismiss(ReasonForDismissingCorrectionPanel reasonForDismissing)
    175 {
    176     if (!m_correctionPanelInfo.isActive)
    177         return;
    178     m_correctionPanelInfo.isActive = false;
    179     m_correctionPanelIsDismissedByEditor = true;
    180     if (client())
    181         client()->dismissCorrectionPanel(reasonForDismissing);
    182 }
    183 
    184 String SpellingCorrectionController::dismissSoon(ReasonForDismissingCorrectionPanel reasonForDismissing)
    185 {
    186     if (!m_correctionPanelInfo.isActive)
    187         return String();
    188     m_correctionPanelInfo.isActive = false;
    189     m_correctionPanelIsDismissedByEditor = true;
    190     if (!client())
    191         return String();
    192     return client()->dismissCorrectionPanelSoon(reasonForDismissing);
    193 }
    194 
    195 void SpellingCorrectionController::applyCorrectionPanelInfo(const Vector<DocumentMarker::MarkerType>& markerTypesToAdd)
    196 {
    197     if (!m_correctionPanelInfo.rangeToBeReplaced)
    198         return;
    199 
    200     ExceptionCode ec = 0;
    201     RefPtr<Range> paragraphRangeContainingCorrection = m_correctionPanelInfo.rangeToBeReplaced->cloneRange(ec);
    202     if (ec)
    203         return;
    204 
    205     setStart(paragraphRangeContainingCorrection.get(), startOfParagraph(m_correctionPanelInfo.rangeToBeReplaced->startPosition()));
    206     setEnd(paragraphRangeContainingCorrection.get(), endOfParagraph(m_correctionPanelInfo.rangeToBeReplaced->endPosition()));
    207 
    208     // After we replace the word at range rangeToBeReplaced, we need to add markers to that range.
    209     // However, once the replacement took place, the value of rangeToBeReplaced is not valid anymore.
    210     // So before we carry out the replacement, we need to store the start position of rangeToBeReplaced
    211     // relative to the start position of the containing paragraph. We use correctionStartOffsetInParagraph
    212     // to store this value. In order to obtain this offset, we need to first create a range
    213     // which spans from the start of paragraph to the start position of rangeToBeReplaced.
    214     RefPtr<Range> correctionStartOffsetInParagraphAsRange = Range::create(paragraphRangeContainingCorrection->startContainer(ec)->document(), paragraphRangeContainingCorrection->startPosition(), paragraphRangeContainingCorrection->startPosition());
    215     if (ec)
    216         return;
    217 
    218     Position startPositionOfRangeToBeReplaced = m_correctionPanelInfo.rangeToBeReplaced->startPosition();
    219     correctionStartOffsetInParagraphAsRange->setEnd(startPositionOfRangeToBeReplaced.containerNode(), startPositionOfRangeToBeReplaced.computeOffsetInContainerNode(), ec);
    220     if (ec)
    221         return;
    222 
    223     // Take note of the location of autocorrection so that we can add marker after the replacement took place.
    224     int correctionStartOffsetInParagraph = TextIterator::rangeLength(correctionStartOffsetInParagraphAsRange.get());
    225 
    226     // Clone the range, since the caller of this method may want to keep the original range around.
    227     RefPtr<Range> rangeToBeReplaced = m_correctionPanelInfo.rangeToBeReplaced->cloneRange(ec);
    228     applyCommand(SpellingCorrectionCommand::create(rangeToBeReplaced, m_correctionPanelInfo.replacementString));
    229     setEnd(paragraphRangeContainingCorrection.get(), m_frame->selection()->selection().start());
    230     RefPtr<Range> replacementRange = TextIterator::subrange(paragraphRangeContainingCorrection.get(), correctionStartOffsetInParagraph,  m_correctionPanelInfo.replacementString.length());
    231     String newText = plainText(replacementRange.get());
    232 
    233     // Check to see if replacement succeeded.
    234     if (newText != m_correctionPanelInfo.replacementString)
    235         return;
    236 
    237     DocumentMarkerController* markers = replacementRange->startContainer()->document()->markers();
    238     size_t size = markerTypesToAdd.size();
    239     for (size_t i = 0; i < size; ++i) {
    240         DocumentMarker::MarkerType markerType = markerTypesToAdd[i];
    241         String description;
    242         if (m_correctionPanelInfo.panelType != CorrectionPanelInfo::PanelTypeReversion && (markerType == DocumentMarker::Replacement || markerType == DocumentMarker::Autocorrected))
    243             description = m_correctionPanelInfo.replacedString;
    244         markers->addMarker(replacementRange.get(), markerType, description);
    245     }
    246 }
    247 
    248 bool SpellingCorrectionController::applyAutocorrectionBeforeTypingIfAppropriate()
    249 {
    250     if (!m_correctionPanelInfo.rangeToBeReplaced || !m_correctionPanelInfo.isActive)
    251         return false;
    252 
    253     if (m_correctionPanelInfo.panelType != CorrectionPanelInfo::PanelTypeCorrection)
    254         return false;
    255 
    256     Position caretPosition = m_frame->selection()->selection().start();
    257 
    258     if (m_correctionPanelInfo.rangeToBeReplaced->endPosition() == caretPosition) {
    259         handleCorrectionPanelResult(dismissSoon(ReasonForDismissingCorrectionPanelAccepted));
    260         return true;
    261     }
    262 
    263     // Pending correction should always be where caret is. But in case this is not always true, we still want to dismiss the panel without accepting the correction.
    264     ASSERT(m_correctionPanelInfo.rangeToBeReplaced->endPosition() == caretPosition);
    265     dismiss(ReasonForDismissingCorrectionPanelIgnored);
    266     return false;
    267 }
    268 
    269 void SpellingCorrectionController::respondToUnappliedSpellCorrection(const VisibleSelection& selectionOfCorrected, const String& corrected, const String& correction)
    270 {
    271     client()->recordAutocorrectionResponse(EditorClient::AutocorrectionReverted, corrected, correction);
    272     m_frame->document()->updateLayout();
    273     m_frame->selection()->setSelection(selectionOfCorrected, SelectionController::CloseTyping | SelectionController::ClearTypingStyle | SelectionController::SpellCorrectionTriggered);
    274     RefPtr<Range> range = Range::create(m_frame->document(), m_frame->selection()->selection().start(), m_frame->selection()->selection().end());
    275 
    276     DocumentMarkerController* markers = m_frame->document()->markers();
    277     markers->removeMarkers(range.get(), DocumentMarker::Spelling | DocumentMarker::Autocorrected, DocumentMarkerController::RemovePartiallyOverlappingMarker);
    278     markers->addMarker(range.get(), DocumentMarker::Replacement);
    279     markers->addMarker(range.get(), DocumentMarker::SpellCheckingExemption);
    280 }
    281 
    282 void SpellingCorrectionController::correctionPanelTimerFired(Timer<SpellingCorrectionController>*)
    283 {
    284     m_correctionPanelIsDismissedByEditor = false;
    285     switch (m_correctionPanelInfo.panelType) {
    286     case CorrectionPanelInfo::PanelTypeCorrection: {
    287         VisibleSelection selection(m_frame->selection()->selection());
    288         VisiblePosition start(selection.start(), selection.affinity());
    289         VisiblePosition p = startOfWord(start, LeftWordIfOnBoundary);
    290         VisibleSelection adjacentWords = VisibleSelection(p, start);
    291         m_frame->editor()->markAllMisspellingsAndBadGrammarInRanges(Editor::MarkSpelling | Editor::ShowCorrectionPanel, adjacentWords.toNormalizedRange().get(), 0);
    292     }
    293         break;
    294     case CorrectionPanelInfo::PanelTypeReversion: {
    295         m_correctionPanelInfo.isActive = true;
    296         m_correctionPanelInfo.replacedString = plainText(m_correctionPanelInfo.rangeToBeReplaced.get());
    297         FloatRect boundingBox = windowRectForRange(m_correctionPanelInfo.rangeToBeReplaced.get());
    298         if (!boundingBox.isEmpty())
    299             client()->showCorrectionPanel(m_correctionPanelInfo.panelType, boundingBox, m_correctionPanelInfo.replacedString, m_correctionPanelInfo.replacementString, Vector<String>());
    300     }
    301         break;
    302     case CorrectionPanelInfo::PanelTypeSpellingSuggestions: {
    303         if (plainText(m_correctionPanelInfo.rangeToBeReplaced.get()) != m_correctionPanelInfo.replacedString)
    304             break;
    305         String paragraphText = plainText(TextCheckingParagraph(m_correctionPanelInfo.rangeToBeReplaced).paragraphRange().get());
    306         Vector<String> suggestions;
    307         textChecker()->getGuessesForWord(m_correctionPanelInfo.replacedString, paragraphText, suggestions);
    308         if (suggestions.isEmpty()) {
    309             m_correctionPanelInfo.rangeToBeReplaced.clear();
    310             break;
    311         }
    312         String topSuggestion = suggestions.first();
    313         suggestions.remove(0);
    314         m_correctionPanelInfo.isActive = true;
    315         FloatRect boundingBox = windowRectForRange(m_correctionPanelInfo.rangeToBeReplaced.get());
    316         if (!boundingBox.isEmpty())
    317             client()->showCorrectionPanel(m_correctionPanelInfo.panelType, boundingBox, m_correctionPanelInfo.replacedString, topSuggestion, suggestions);
    318     }
    319         break;
    320     }
    321 }
    322 
    323 void SpellingCorrectionController::handleCorrectionPanelResult(const String& correction)
    324 {
    325     Range* replacedRange = m_correctionPanelInfo.rangeToBeReplaced.get();
    326     if (!replacedRange || m_frame->document() != replacedRange->ownerDocument())
    327         return;
    328 
    329     String currentWord = plainText(m_correctionPanelInfo.rangeToBeReplaced.get());
    330     // Check to see if the word we are about to correct has been changed between timer firing and callback being triggered.
    331     if (currentWord != m_correctionPanelInfo.replacedString)
    332         return;
    333 
    334     m_correctionPanelInfo.isActive = false;
    335 
    336     switch (m_correctionPanelInfo.panelType) {
    337     case CorrectionPanelInfo::PanelTypeCorrection:
    338         if (correction.length()) {
    339             m_correctionPanelInfo.replacementString = correction;
    340             applyCorrectionPanelInfo(markerTypesForAutocorrection());
    341         } else if (!m_correctionPanelIsDismissedByEditor)
    342             replacedRange->startContainer()->document()->markers()->addMarker(replacedRange, DocumentMarker::RejectedCorrection, m_correctionPanelInfo.replacedString);
    343         break;
    344     case CorrectionPanelInfo::PanelTypeReversion:
    345     case CorrectionPanelInfo::PanelTypeSpellingSuggestions:
    346         if (correction.length()) {
    347             m_correctionPanelInfo.replacementString = correction;
    348             applyCorrectionPanelInfo(markerTypesForReplacement());
    349         }
    350         break;
    351     }
    352 
    353     m_correctionPanelInfo.rangeToBeReplaced.clear();
    354 }
    355 
    356 bool SpellingCorrectionController::isAutomaticSpellingCorrectionEnabled()
    357 {
    358     return client() && client()->isAutomaticSpellingCorrectionEnabled();
    359 }
    360 
    361 FloatRect SpellingCorrectionController::windowRectForRange(const Range* range) const
    362 {
    363     FrameView* view = m_frame->view();
    364     return view ? view->contentsToWindow(IntRect(range->boundingRect())) : FloatRect();
    365 }
    366 
    367 void SpellingCorrectionController::respondToChangedSelection(const VisibleSelection& oldSelection)
    368 {
    369     VisibleSelection currentSelection(m_frame->selection()->selection());
    370     // When user moves caret to the end of autocorrected word and pauses, we show the panel
    371     // containing the original pre-correction word so that user can quickly revert the
    372     // undesired autocorrection. Here, we start correction panel timer once we confirm that
    373     // the new caret position is at the end of a word.
    374     if (!currentSelection.isCaret() || currentSelection == oldSelection)
    375         return;
    376 
    377     VisiblePosition selectionPosition = currentSelection.start();
    378     VisiblePosition endPositionOfWord = endOfWord(selectionPosition, LeftWordIfOnBoundary);
    379     if (selectionPosition != endPositionOfWord)
    380         return;
    381 
    382     Position position = endPositionOfWord.deepEquivalent();
    383     if (position.anchorType() != Position::PositionIsOffsetInAnchor)
    384         return;
    385 
    386     Node* node = position.containerNode();
    387     int endOffset = position.offsetInContainerNode();
    388     Vector<DocumentMarker> markers = node->document()->markers()->markersForNode(node);
    389     size_t markerCount = markers.size();
    390     for (size_t i = 0; i < markerCount; ++i) {
    391         const DocumentMarker& marker = markers[i];
    392         if (!shouldStartTimeFor(marker, endOffset))
    393             continue;
    394         RefPtr<Range> wordRange = Range::create(m_frame->document(), node, marker.startOffset, node, marker.endOffset);
    395         String currentWord = plainText(wordRange.get());
    396         if (!currentWord.length())
    397             continue;
    398 
    399         m_correctionPanelInfo.rangeToBeReplaced = wordRange;
    400         m_correctionPanelInfo.replacedString = currentWord;
    401         if (marker.type == DocumentMarker::Spelling)
    402             startCorrectionPanelTimer(CorrectionPanelInfo::PanelTypeSpellingSuggestions);
    403         else {
    404             m_correctionPanelInfo.replacementString = marker.description;
    405             startCorrectionPanelTimer(CorrectionPanelInfo::PanelTypeReversion);
    406         }
    407 
    408         break;
    409     }
    410 }
    411 
    412 void SpellingCorrectionController::respondToAppliedEditing(PassRefPtr<EditCommand> command)
    413 {
    414     if (command->isTopLevelCommand() && !command->shouldRetainAutocorrectionIndicator())
    415         m_frame->document()->markers()->removeMarkers(DocumentMarker::CorrectionIndicator);
    416 }
    417 
    418 EditorClient* SpellingCorrectionController::client()
    419 {
    420     return m_frame->page() ? m_frame->page()->editorClient() : 0;
    421 }
    422 
    423 TextCheckerClient* SpellingCorrectionController::textChecker()
    424 {
    425     if (EditorClient* owner = client())
    426         return owner->textChecker();
    427     return 0;
    428 }
    429 
    430 void SpellingCorrectionController::recordAutocorrectionResponseReversed(const String& replacedString, const String& replacementString)
    431 {
    432     client()->recordAutocorrectionResponse(EditorClient::AutocorrectionReverted, replacedString, replacementString);
    433 }
    434 
    435 void SpellingCorrectionController::recordAutocorrectionResponseReversed(const String& replacedString, PassRefPtr<Range> replacementRange)
    436 {
    437     recordAutocorrectionResponseReversed(replacedString, plainText(replacementRange.get()));
    438 }
    439 
    440 void SpellingCorrectionController::markReversed(PassRefPtr<Range> changedRange)
    441 {
    442     changedRange->startContainer()->document()->markers()->removeMarkers(changedRange.get(), DocumentMarker::Autocorrected, DocumentMarkerController::RemovePartiallyOverlappingMarker);
    443     changedRange->startContainer()->document()->markers()->addMarker(changedRange.get(), DocumentMarker::SpellCheckingExemption);
    444 }
    445 
    446 void SpellingCorrectionController::markCorrection(PassRefPtr<Range> replacedRange, const String& replacedString)
    447 {
    448     Vector<DocumentMarker::MarkerType> markerTypesToAdd = markerTypesForAutocorrection();
    449     DocumentMarkerController* markers = replacedRange->startContainer()->document()->markers();
    450     for (size_t i = 0; i < markerTypesToAdd.size(); ++i) {
    451         DocumentMarker::MarkerType markerType = markerTypesToAdd[i];
    452         if (markerType == DocumentMarker::Replacement || markerType == DocumentMarker::Autocorrected)
    453             markers->addMarker(replacedRange.get(), markerType, replacedString);
    454         else
    455             markers->addMarker(replacedRange.get(), markerType);
    456     }
    457 }
    458 
    459 void SpellingCorrectionController::recordSpellcheckerResponseForModifiedCorrection(Range* rangeOfCorrection, const String& corrected, const String& correction)
    460 {
    461     if (!rangeOfCorrection)
    462         return;
    463     DocumentMarkerController* markers = rangeOfCorrection->startContainer()->document()->markers();
    464     Vector<DocumentMarker> correctedOnceMarkers = markers->markersInRange(rangeOfCorrection, DocumentMarker::Autocorrected);
    465     if (correctedOnceMarkers.isEmpty())
    466         return;
    467 
    468     // Spelling corrected text has been edited. We need to determine whether user has reverted it to original text or
    469     // edited it to something else, and notify spellchecker accordingly.
    470     if (markersHaveIdenticalDescription(correctedOnceMarkers) && correctedOnceMarkers[0].description == corrected)
    471         client()->recordAutocorrectionResponse(EditorClient::AutocorrectionReverted, corrected, correction);
    472     else
    473         client()->recordAutocorrectionResponse(EditorClient::AutocorrectionEdited, corrected, correction);
    474     markers->removeMarkers(rangeOfCorrection, DocumentMarker::Autocorrected, DocumentMarkerController::RemovePartiallyOverlappingMarker);
    475 }
    476 
    477 #endif
    478 
    479 } // namespace WebCore
    480