1 /* 2 * Copyright (C) 2006, 2007 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 "core/editing/TextCheckingHelper.h" 29 30 #include "bindings/core/v8/ExceptionState.h" 31 #include "bindings/core/v8/ExceptionStatePlaceholder.h" 32 #include "core/dom/Document.h" 33 #include "core/dom/DocumentMarkerController.h" 34 #include "core/dom/Range.h" 35 #include "core/editing/TextIterator.h" 36 #include "core/editing/VisiblePosition.h" 37 #include "core/editing/VisibleUnits.h" 38 #include "core/frame/LocalFrame.h" 39 #include "core/frame/Settings.h" 40 #include "core/page/SpellCheckerClient.h" 41 #include "platform/text/TextBreakIterator.h" 42 #include "platform/text/TextCheckerClient.h" 43 44 namespace blink { 45 46 static void findBadGrammars(TextCheckerClient& client, const UChar* text, int start, int length, Vector<TextCheckingResult>& results) 47 { 48 int checkLocation = start; 49 int checkLength = length; 50 51 while (0 < checkLength) { 52 int badGrammarLocation = -1; 53 int badGrammarLength = 0; 54 Vector<GrammarDetail> badGrammarDetails; 55 client.checkGrammarOfString(String(text + checkLocation, checkLength), badGrammarDetails, &badGrammarLocation, &badGrammarLength); 56 if (!badGrammarLength) 57 break; 58 ASSERT(0 <= badGrammarLocation && badGrammarLocation <= checkLength); 59 ASSERT(0 < badGrammarLength && badGrammarLocation + badGrammarLength <= checkLength); 60 TextCheckingResult badGrammar; 61 badGrammar.decoration = TextDecorationTypeGrammar; 62 badGrammar.location = checkLocation + badGrammarLocation; 63 badGrammar.length = badGrammarLength; 64 badGrammar.details.swap(badGrammarDetails); 65 results.append(badGrammar); 66 67 checkLocation += (badGrammarLocation + badGrammarLength); 68 checkLength -= (badGrammarLocation + badGrammarLength); 69 } 70 } 71 72 static void findMisspellings(TextCheckerClient& client, const UChar* text, int start, int length, Vector<TextCheckingResult>& results) 73 { 74 TextBreakIterator* iterator = wordBreakIterator(text + start, length); 75 if (!iterator) 76 return; 77 int wordStart = iterator->current(); 78 while (0 <= wordStart) { 79 int wordEnd = iterator->next(); 80 if (wordEnd < 0) 81 break; 82 int wordLength = wordEnd - wordStart; 83 int misspellingLocation = -1; 84 int misspellingLength = 0; 85 client.checkSpellingOfString(String(text + start + wordStart, wordLength), &misspellingLocation, &misspellingLength); 86 if (0 < misspellingLength) { 87 ASSERT(0 <= misspellingLocation && misspellingLocation <= wordLength); 88 ASSERT(0 < misspellingLength && misspellingLocation + misspellingLength <= wordLength); 89 TextCheckingResult misspelling; 90 misspelling.decoration = TextDecorationTypeSpelling; 91 misspelling.location = start + wordStart + misspellingLocation; 92 misspelling.length = misspellingLength; 93 misspelling.replacement = client.getAutoCorrectSuggestionForMisspelledWord(String(text + misspelling.location, misspelling.length)); 94 results.append(misspelling); 95 } 96 97 wordStart = wordEnd; 98 } 99 } 100 101 static PassRefPtrWillBeRawPtr<Range> expandToParagraphBoundary(PassRefPtrWillBeRawPtr<Range> range) 102 { 103 RefPtrWillBeRawPtr<Range> paragraphRange = range->cloneRange(); 104 setStart(paragraphRange.get(), startOfParagraph(VisiblePosition(range->startPosition()))); 105 setEnd(paragraphRange.get(), endOfParagraph(VisiblePosition(range->endPosition()))); 106 return paragraphRange; 107 } 108 109 TextCheckingParagraph::TextCheckingParagraph(PassRefPtrWillBeRawPtr<Range> checkingRange) 110 : m_checkingRange(checkingRange) 111 , m_checkingStart(-1) 112 , m_checkingEnd(-1) 113 , m_checkingLength(-1) 114 { 115 } 116 117 TextCheckingParagraph::TextCheckingParagraph(PassRefPtrWillBeRawPtr<Range> checkingRange, PassRefPtrWillBeRawPtr<Range> paragraphRange) 118 : m_checkingRange(checkingRange) 119 , m_paragraphRange(paragraphRange) 120 , m_checkingStart(-1) 121 , m_checkingEnd(-1) 122 , m_checkingLength(-1) 123 { 124 } 125 126 TextCheckingParagraph::~TextCheckingParagraph() 127 { 128 } 129 130 void TextCheckingParagraph::expandRangeToNextEnd() 131 { 132 ASSERT(m_checkingRange); 133 setEnd(paragraphRange().get(), endOfParagraph(startOfNextParagraph(VisiblePosition(paragraphRange()->startPosition())))); 134 invalidateParagraphRangeValues(); 135 } 136 137 void TextCheckingParagraph::invalidateParagraphRangeValues() 138 { 139 m_checkingStart = m_checkingEnd = -1; 140 m_offsetAsRange = nullptr; 141 m_text = String(); 142 } 143 144 int TextCheckingParagraph::rangeLength() const 145 { 146 ASSERT(m_checkingRange); 147 return TextIterator::rangeLength(paragraphRange().get()); 148 } 149 150 PassRefPtrWillBeRawPtr<Range> TextCheckingParagraph::paragraphRange() const 151 { 152 ASSERT(m_checkingRange); 153 if (!m_paragraphRange) 154 m_paragraphRange = expandToParagraphBoundary(checkingRange()); 155 return m_paragraphRange; 156 } 157 158 PassRefPtrWillBeRawPtr<Range> TextCheckingParagraph::subrange(int characterOffset, int characterCount) const 159 { 160 ASSERT(m_checkingRange); 161 return TextIterator::subrange(paragraphRange().get(), characterOffset, characterCount); 162 } 163 164 int TextCheckingParagraph::offsetTo(const Position& position, ExceptionState& exceptionState) const 165 { 166 ASSERT(m_checkingRange); 167 RefPtrWillBeRawPtr<Range> range = offsetAsRange()->cloneRange(); 168 range->setEnd(position.containerNode(), position.computeOffsetInContainerNode(), exceptionState); 169 if (exceptionState.hadException()) 170 return 0; 171 return TextIterator::rangeLength(range.get()); 172 } 173 174 bool TextCheckingParagraph::isEmpty() const 175 { 176 // Both predicates should have same result, but we check both just for sure. 177 // We need to investigate to remove this redundancy. 178 return isRangeEmpty() || isTextEmpty(); 179 } 180 181 PassRefPtrWillBeRawPtr<Range> TextCheckingParagraph::offsetAsRange() const 182 { 183 ASSERT(m_checkingRange); 184 if (!m_offsetAsRange) 185 m_offsetAsRange = Range::create(paragraphRange()->startContainer()->document(), paragraphRange()->startPosition(), checkingRange()->startPosition()); 186 187 return m_offsetAsRange; 188 } 189 190 const String& TextCheckingParagraph::text() const 191 { 192 ASSERT(m_checkingRange); 193 if (m_text.isEmpty()) 194 m_text = plainText(paragraphRange().get()); 195 return m_text; 196 } 197 198 int TextCheckingParagraph::checkingStart() const 199 { 200 ASSERT(m_checkingRange); 201 if (m_checkingStart == -1) 202 m_checkingStart = TextIterator::rangeLength(offsetAsRange().get()); 203 return m_checkingStart; 204 } 205 206 int TextCheckingParagraph::checkingEnd() const 207 { 208 ASSERT(m_checkingRange); 209 if (m_checkingEnd == -1) 210 m_checkingEnd = checkingStart() + TextIterator::rangeLength(checkingRange().get()); 211 return m_checkingEnd; 212 } 213 214 int TextCheckingParagraph::checkingLength() const 215 { 216 ASSERT(m_checkingRange); 217 if (-1 == m_checkingLength) 218 m_checkingLength = TextIterator::rangeLength(checkingRange().get()); 219 return m_checkingLength; 220 } 221 222 TextCheckingHelper::TextCheckingHelper(SpellCheckerClient& client, const Position& start, const Position& end) 223 : m_client(&client) 224 , m_start(start) 225 , m_end(end) 226 { 227 } 228 229 TextCheckingHelper::~TextCheckingHelper() 230 { 231 } 232 233 String TextCheckingHelper::findFirstMisspelling(int& firstMisspellingOffset, bool markAll, RefPtrWillBeRawPtr<Range>& firstMisspellingRange) 234 { 235 WordAwareIterator it(m_start, m_end); 236 firstMisspellingOffset = 0; 237 238 String firstMisspelling; 239 int currentChunkOffset = 0; 240 241 while (!it.atEnd()) { 242 int length = it.length(); 243 244 // Skip some work for one-space-char hunks 245 if (!(length == 1 && it.characterAt(0) == ' ')) { 246 247 int misspellingLocation = -1; 248 int misspellingLength = 0; 249 m_client->textChecker().checkSpellingOfString(it.substring(0, length), &misspellingLocation, &misspellingLength); 250 251 // 5490627 shows that there was some code path here where the String constructor below crashes. 252 // We don't know exactly what combination of bad input caused this, so we're making this much 253 // more robust against bad input on release builds. 254 ASSERT(misspellingLength >= 0); 255 ASSERT(misspellingLocation >= -1); 256 ASSERT(!misspellingLength || misspellingLocation >= 0); 257 ASSERT(misspellingLocation < length); 258 ASSERT(misspellingLength <= length); 259 ASSERT(misspellingLocation + misspellingLength <= length); 260 261 if (misspellingLocation >= 0 && misspellingLength > 0 && misspellingLocation < length && misspellingLength <= length && misspellingLocation + misspellingLength <= length) { 262 263 // Compute range of misspelled word 264 Position misspellingStart = m_start; 265 Position misspellingEnd = m_end; 266 TextIterator::subrange(misspellingStart, misspellingEnd, currentChunkOffset + misspellingLocation, misspellingLength); 267 268 // Remember first-encountered misspelling and its offset. 269 if (!firstMisspelling) { 270 firstMisspellingOffset = currentChunkOffset + misspellingLocation; 271 firstMisspelling = it.substring(misspellingLocation, misspellingLength); 272 firstMisspellingRange = Range::create(misspellingStart.containerNode()->document(), m_start, m_end); 273 } 274 275 // Store marker for misspelled word. 276 misspellingStart.containerNode()->document().markers().addMarker(misspellingStart, misspellingEnd, DocumentMarker::Spelling); 277 278 // Bail out if we're marking only the first misspelling, and not all instances. 279 if (!markAll) 280 break; 281 } 282 } 283 284 currentChunkOffset += length; 285 it.advance(); 286 } 287 288 return firstMisspelling; 289 } 290 291 String TextCheckingHelper::findFirstMisspellingOrBadGrammar(bool checkGrammar, bool& outIsSpelling, int& outFirstFoundOffset, GrammarDetail& outGrammarDetail) 292 { 293 if (!unifiedTextCheckerEnabled()) 294 return ""; 295 296 String firstFoundItem; 297 String misspelledWord; 298 String badGrammarPhrase; 299 300 // Initialize out parameters; these will be updated if we find something to return. 301 outIsSpelling = true; 302 outFirstFoundOffset = 0; 303 outGrammarDetail.location = -1; 304 outGrammarDetail.length = 0; 305 outGrammarDetail.guesses.clear(); 306 outGrammarDetail.userDescription = ""; 307 308 // Expand the search range to encompass entire paragraphs, since text checking needs that much context. 309 // Determine the character offset from the start of the paragraph to the start of the original search range, 310 // since we will want to ignore results in this area. 311 Position paragraphStart = startOfParagraph(VisiblePosition(m_start)).toParentAnchoredPosition(); 312 Position paragraphEnd = m_end; 313 int totalRangeLength = TextIterator::rangeLength(paragraphStart, paragraphEnd); 314 paragraphEnd = endOfParagraph(VisiblePosition(m_start)).toParentAnchoredPosition(); 315 316 int rangeStartOffset = TextIterator::rangeLength(paragraphStart, m_start); 317 int totalLengthProcessed = 0; 318 319 bool firstIteration = true; 320 bool lastIteration = false; 321 while (totalLengthProcessed < totalRangeLength) { 322 // Iterate through the search range by paragraphs, checking each one for spelling and grammar. 323 int currentLength = TextIterator::rangeLength(paragraphStart, paragraphEnd); 324 int currentStartOffset = firstIteration ? rangeStartOffset : 0; 325 int currentEndOffset = currentLength; 326 if (inSameParagraph(VisiblePosition(paragraphStart), VisiblePosition(m_end))) { 327 // Determine the character offset from the end of the original search range to the end of the paragraph, 328 // since we will want to ignore results in this area. 329 currentEndOffset = TextIterator::rangeLength(paragraphStart, m_end); 330 lastIteration = true; 331 } 332 if (currentStartOffset < currentEndOffset) { 333 String paragraphString = plainText(paragraphStart, paragraphEnd); 334 if (paragraphString.length() > 0) { 335 bool foundGrammar = false; 336 int spellingLocation = 0; 337 int grammarPhraseLocation = 0; 338 int grammarDetailLocation = 0; 339 unsigned grammarDetailIndex = 0; 340 341 Vector<TextCheckingResult> results; 342 TextCheckingTypeMask checkingTypes = checkGrammar ? (TextCheckingTypeSpelling | TextCheckingTypeGrammar) : TextCheckingTypeSpelling; 343 checkTextOfParagraph(m_client->textChecker(), paragraphString, checkingTypes, results); 344 345 for (unsigned i = 0; i < results.size(); i++) { 346 const TextCheckingResult* result = &results[i]; 347 if (result->decoration == TextDecorationTypeSpelling && result->location >= currentStartOffset && result->location + result->length <= currentEndOffset) { 348 ASSERT(result->length > 0 && result->location >= 0); 349 spellingLocation = result->location; 350 misspelledWord = paragraphString.substring(result->location, result->length); 351 ASSERT(misspelledWord.length()); 352 break; 353 } 354 if (checkGrammar && result->decoration == TextDecorationTypeGrammar && result->location < currentEndOffset && result->location + result->length > currentStartOffset) { 355 ASSERT(result->length > 0 && result->location >= 0); 356 // We can't stop after the first grammar result, since there might still be a spelling result after 357 // it begins but before the first detail in it, but we can stop if we find a second grammar result. 358 if (foundGrammar) 359 break; 360 for (unsigned j = 0; j < result->details.size(); j++) { 361 const GrammarDetail* detail = &result->details[j]; 362 ASSERT(detail->length > 0 && detail->location >= 0); 363 if (result->location + detail->location >= currentStartOffset && result->location + detail->location + detail->length <= currentEndOffset && (!foundGrammar || result->location + detail->location < grammarDetailLocation)) { 364 grammarDetailIndex = j; 365 grammarDetailLocation = result->location + detail->location; 366 foundGrammar = true; 367 } 368 } 369 if (foundGrammar) { 370 grammarPhraseLocation = result->location; 371 outGrammarDetail = result->details[grammarDetailIndex]; 372 badGrammarPhrase = paragraphString.substring(result->location, result->length); 373 ASSERT(badGrammarPhrase.length()); 374 } 375 } 376 } 377 378 if (!misspelledWord.isEmpty() && (!checkGrammar || badGrammarPhrase.isEmpty() || spellingLocation <= grammarDetailLocation)) { 379 int spellingOffset = spellingLocation - currentStartOffset; 380 if (!firstIteration) 381 spellingOffset += TextIterator::rangeLength(m_start, paragraphStart); 382 outIsSpelling = true; 383 outFirstFoundOffset = spellingOffset; 384 firstFoundItem = misspelledWord; 385 break; 386 } 387 if (checkGrammar && !badGrammarPhrase.isEmpty()) { 388 int grammarPhraseOffset = grammarPhraseLocation - currentStartOffset; 389 if (!firstIteration) 390 grammarPhraseOffset += TextIterator::rangeLength(m_start, paragraphStart); 391 outIsSpelling = false; 392 outFirstFoundOffset = grammarPhraseOffset; 393 firstFoundItem = badGrammarPhrase; 394 break; 395 } 396 } 397 } 398 if (lastIteration || totalLengthProcessed + currentLength >= totalRangeLength) 399 break; 400 VisiblePosition newParagraphStart = startOfNextParagraph(VisiblePosition(paragraphEnd)); 401 paragraphStart = newParagraphStart.toParentAnchoredPosition(); 402 paragraphEnd = endOfParagraph(newParagraphStart).toParentAnchoredPosition(); 403 firstIteration = false; 404 totalLengthProcessed += currentLength; 405 } 406 return firstFoundItem; 407 } 408 409 int TextCheckingHelper::findFirstGrammarDetail(const Vector<GrammarDetail>& grammarDetails, int badGrammarPhraseLocation, int startOffset, int endOffset, bool markAll) const 410 { 411 // Found some bad grammar. Find the earliest detail range that starts in our search range (if any). 412 // Optionally add a DocumentMarker for each detail in the range. 413 int earliestDetailLocationSoFar = -1; 414 int earliestDetailIndex = -1; 415 for (unsigned i = 0; i < grammarDetails.size(); i++) { 416 const GrammarDetail* detail = &grammarDetails[i]; 417 ASSERT(detail->length > 0 && detail->location >= 0); 418 419 int detailStartOffsetInParagraph = badGrammarPhraseLocation + detail->location; 420 421 // Skip this detail if it starts before the original search range 422 if (detailStartOffsetInParagraph < startOffset) 423 continue; 424 425 // Skip this detail if it starts after the original search range 426 if (detailStartOffsetInParagraph >= endOffset) 427 continue; 428 429 if (markAll) { 430 Position badGrammarStart = m_start; 431 Position badGrammarEnd = m_end; 432 TextIterator::subrange(badGrammarStart, badGrammarEnd, badGrammarPhraseLocation - startOffset + detail->location, detail->length); 433 badGrammarStart.containerNode()->document().markers().addMarker(badGrammarStart, badGrammarEnd, DocumentMarker::Grammar, detail->userDescription); 434 } 435 436 // Remember this detail only if it's earlier than our current candidate (the details aren't in a guaranteed order) 437 if (earliestDetailIndex < 0 || earliestDetailLocationSoFar > detail->location) { 438 earliestDetailIndex = i; 439 earliestDetailLocationSoFar = detail->location; 440 } 441 } 442 443 return earliestDetailIndex; 444 } 445 446 String TextCheckingHelper::findFirstBadGrammar(GrammarDetail& outGrammarDetail, int& outGrammarPhraseOffset, bool markAll) 447 { 448 // Initialize out parameters; these will be updated if we find something to return. 449 outGrammarDetail.location = -1; 450 outGrammarDetail.length = 0; 451 outGrammarDetail.guesses.clear(); 452 outGrammarDetail.userDescription = ""; 453 outGrammarPhraseOffset = 0; 454 455 String firstBadGrammarPhrase; 456 457 // Expand the search range to encompass entire paragraphs, since grammar checking needs that much context. 458 // Determine the character offset from the start of the paragraph to the start of the original search range, 459 // since we will want to ignore results in this area. 460 TextCheckingParagraph paragraph(Range::create(m_start.containerNode()->document(), m_start, m_end)); 461 462 // Start checking from beginning of paragraph, but skip past results that occur before the start of the original search range. 463 int startOffset = 0; 464 while (startOffset < paragraph.checkingEnd()) { 465 Vector<GrammarDetail> grammarDetails; 466 int badGrammarPhraseLocation = -1; 467 int badGrammarPhraseLength = 0; 468 m_client->textChecker().checkGrammarOfString(paragraph.textSubstring(startOffset), grammarDetails, &badGrammarPhraseLocation, &badGrammarPhraseLength); 469 470 if (!badGrammarPhraseLength) { 471 ASSERT(badGrammarPhraseLocation == -1); 472 return String(); 473 } 474 475 ASSERT(badGrammarPhraseLocation >= 0); 476 badGrammarPhraseLocation += startOffset; 477 478 479 // Found some bad grammar. Find the earliest detail range that starts in our search range (if any). 480 int badGrammarIndex = findFirstGrammarDetail(grammarDetails, badGrammarPhraseLocation, paragraph.checkingStart(), paragraph.checkingEnd(), markAll); 481 if (badGrammarIndex >= 0) { 482 ASSERT(static_cast<unsigned>(badGrammarIndex) < grammarDetails.size()); 483 outGrammarDetail = grammarDetails[badGrammarIndex]; 484 } 485 486 // If we found a detail in range, then we have found the first bad phrase (unless we found one earlier but 487 // kept going so we could mark all instances). 488 if (badGrammarIndex >= 0 && firstBadGrammarPhrase.isEmpty()) { 489 outGrammarPhraseOffset = badGrammarPhraseLocation - paragraph.checkingStart(); 490 firstBadGrammarPhrase = paragraph.textSubstring(badGrammarPhraseLocation, badGrammarPhraseLength); 491 492 // Found one. We're done now, unless we're marking each instance. 493 if (!markAll) 494 break; 495 } 496 497 // These results were all between the start of the paragraph and the start of the search range; look 498 // beyond this phrase. 499 startOffset = badGrammarPhraseLocation + badGrammarPhraseLength; 500 } 501 502 return firstBadGrammarPhrase; 503 } 504 505 void TextCheckingHelper::markAllMisspellings(RefPtrWillBeRawPtr<Range>& firstMisspellingRange) 506 { 507 // Use the "markAll" feature of findFirstMisspelling. Ignore the return value and the "out parameter"; 508 // all we need to do is mark every instance. 509 int ignoredOffset; 510 findFirstMisspelling(ignoredOffset, true, firstMisspellingRange); 511 } 512 513 void TextCheckingHelper::markAllBadGrammar() 514 { 515 // Use the "markAll" feature of ofindFirstBadGrammar. Ignore the return value and "out parameters"; all we need to 516 // do is mark every instance. 517 GrammarDetail ignoredGrammarDetail; 518 int ignoredOffset; 519 findFirstBadGrammar(ignoredGrammarDetail, ignoredOffset, true); 520 } 521 522 bool TextCheckingHelper::unifiedTextCheckerEnabled() const 523 { 524 ASSERT(m_start.isNotNull()); 525 Document& doc = m_start.containerNode()->document(); 526 return blink::unifiedTextCheckerEnabled(doc.frame()); 527 } 528 529 void checkTextOfParagraph(TextCheckerClient& client, const String& text, TextCheckingTypeMask checkingTypes, Vector<TextCheckingResult>& results) 530 { 531 Vector<UChar> characters; 532 text.appendTo(characters); 533 unsigned length = text.length(); 534 535 Vector<TextCheckingResult> spellingResult; 536 if (checkingTypes & TextCheckingTypeSpelling) 537 findMisspellings(client, characters.data(), 0, length, spellingResult); 538 539 Vector<TextCheckingResult> grammarResult; 540 if (checkingTypes & TextCheckingTypeGrammar) { 541 // Only checks grammartical error before the first misspellings 542 int grammarCheckLength = length; 543 for (size_t i = 0; i < spellingResult.size(); ++i) { 544 if (spellingResult[i].location < grammarCheckLength) 545 grammarCheckLength = spellingResult[i].location; 546 } 547 548 findBadGrammars(client, characters.data(), 0, grammarCheckLength, grammarResult); 549 } 550 551 if (grammarResult.size()) 552 results.swap(grammarResult); 553 554 if (spellingResult.size()) { 555 if (results.isEmpty()) 556 results.swap(spellingResult); 557 else 558 results.appendVector(spellingResult); 559 } 560 } 561 562 bool unifiedTextCheckerEnabled(const LocalFrame* frame) 563 { 564 if (!frame) 565 return false; 566 567 const Settings* settings = frame->settings(); 568 if (!settings) 569 return false; 570 571 return settings->unifiedTextCheckerEnabled(); 572 } 573 574 } 575