1 /* 2 * Copyright (C) 2007 Alp Toker <alp (at) atoker.com> 3 * Copyright (C) 2008 Nuanti Ltd. 4 * Copyright (C) 2009 Diego Escalante Urrelo <diegoe (at) gnome.org> 5 * Copyright (C) 2006, 2007 Apple Inc. All rights reserved. 6 * Copyright (C) 2009, Igalia S.L. 7 * 8 * This library is free software; you can redistribute it and/or 9 * modify it under the terms of the GNU Lesser General Public 10 * License as published by the Free Software Foundation; either 11 * version 2 of the License, or (at your option) any later version. 12 * 13 * This library is distributed in the hope that it will be useful, 14 * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 16 * Lesser General Public License for more details. 17 * 18 * You should have received a copy of the GNU Lesser General Public 19 * License along with this library; if not, write to the Free Software 20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 21 */ 22 23 #include "config.h" 24 #include "EditorClientGtk.h" 25 26 #include "CString.h" 27 #include "DataObjectGtk.h" 28 #include "EditCommand.h" 29 #include "Editor.h" 30 #include <enchant.h> 31 #include "EventNames.h" 32 #include "FocusController.h" 33 #include "Frame.h" 34 #include <glib.h> 35 #include "KeyboardCodes.h" 36 #include "KeyboardEvent.h" 37 #include "NotImplemented.h" 38 #include "Page.h" 39 #include "PasteboardHelperGtk.h" 40 #include "PlatformKeyboardEvent.h" 41 #include "markup.h" 42 #include "webkitprivate.h" 43 44 // Arbitrary depth limit for the undo stack, to keep it from using 45 // unbounded memory. This is the maximum number of distinct undoable 46 // actions -- unbroken stretches of typed characters are coalesced 47 // into a single action. 48 #define maximumUndoStackDepth 1000 49 50 using namespace WebCore; 51 52 namespace WebKit { 53 54 static gchar* pendingComposition = 0; 55 static gchar* pendingPreedit = 0; 56 57 static void setPendingComposition(gchar* newComposition) 58 { 59 g_free(pendingComposition); 60 pendingComposition = newComposition; 61 } 62 63 static void setPendingPreedit(gchar* newPreedit) 64 { 65 g_free(pendingPreedit); 66 pendingPreedit = newPreedit; 67 } 68 69 static void clearPendingIMData() 70 { 71 setPendingComposition(0); 72 setPendingPreedit(0); 73 } 74 static void imContextCommitted(GtkIMContext* context, const gchar* str, EditorClient* client) 75 { 76 // This signal will fire during a keydown event. We want the contents of the 77 // field to change right before the keyup event, so we wait until then to actually 78 // commit this composition. 79 setPendingComposition(g_strdup(str)); 80 } 81 82 static void imContextPreeditChanged(GtkIMContext* context, EditorClient* client) 83 { 84 // We ignore the provided PangoAttrList for now. 85 gchar* newPreedit = 0; 86 gtk_im_context_get_preedit_string(context, &newPreedit, NULL, NULL); 87 setPendingPreedit(newPreedit); 88 } 89 90 void EditorClient::setInputMethodState(bool active) 91 { 92 WebKitWebViewPrivate* priv = m_webView->priv; 93 94 if (active) 95 gtk_im_context_focus_in(priv->imContext); 96 else 97 gtk_im_context_focus_out(priv->imContext); 98 99 #ifdef MAEMO_CHANGES 100 if (active) 101 hildon_gtk_im_context_show(priv->imContext); 102 else 103 hildon_gtk_im_context_hide(priv->imContext); 104 #endif 105 } 106 107 bool EditorClient::shouldDeleteRange(Range*) 108 { 109 notImplemented(); 110 return true; 111 } 112 113 bool EditorClient::shouldShowDeleteInterface(HTMLElement*) 114 { 115 return false; 116 } 117 118 bool EditorClient::isContinuousSpellCheckingEnabled() 119 { 120 WebKitWebSettings* settings = webkit_web_view_get_settings(m_webView); 121 122 gboolean enabled; 123 g_object_get(settings, "enable-spell-checking", &enabled, NULL); 124 125 return enabled; 126 } 127 128 bool EditorClient::isGrammarCheckingEnabled() 129 { 130 notImplemented(); 131 return false; 132 } 133 134 int EditorClient::spellCheckerDocumentTag() 135 { 136 notImplemented(); 137 return 0; 138 } 139 140 bool EditorClient::shouldBeginEditing(WebCore::Range*) 141 { 142 clearPendingIMData(); 143 144 notImplemented(); 145 return true; 146 } 147 148 bool EditorClient::shouldEndEditing(WebCore::Range*) 149 { 150 clearPendingIMData(); 151 152 notImplemented(); 153 return true; 154 } 155 156 bool EditorClient::shouldInsertText(const String&, Range*, EditorInsertAction) 157 { 158 notImplemented(); 159 return true; 160 } 161 162 bool EditorClient::shouldChangeSelectedRange(Range*, Range*, EAffinity, bool) 163 { 164 notImplemented(); 165 return true; 166 } 167 168 bool EditorClient::shouldApplyStyle(WebCore::CSSStyleDeclaration*, WebCore::Range*) 169 { 170 notImplemented(); 171 return true; 172 } 173 174 bool EditorClient::shouldMoveRangeAfterDelete(WebCore::Range*, WebCore::Range*) 175 { 176 notImplemented(); 177 return true; 178 } 179 180 void EditorClient::didBeginEditing() 181 { 182 notImplemented(); 183 } 184 185 void EditorClient::respondToChangedContents() 186 { 187 notImplemented(); 188 } 189 190 void EditorClient::respondToChangedSelection() 191 { 192 WebKitWebViewPrivate* priv = m_webView->priv; 193 WebCore::Page* corePage = core(m_webView); 194 Frame* targetFrame = corePage->focusController()->focusedOrMainFrame(); 195 196 if (!targetFrame) 197 return; 198 199 if (targetFrame->editor()->ignoreCompositionSelectionChange()) 200 return; 201 202 #if PLATFORM(X11) 203 GtkClipboard* clipboard = gtk_widget_get_clipboard(GTK_WIDGET(m_webView), GDK_SELECTION_PRIMARY); 204 DataObjectGtk* dataObject = DataObjectGtk::forClipboard(clipboard); 205 206 if (targetFrame->selection()->isRange()) { 207 dataObject->clear(); 208 dataObject->setRange(targetFrame->selection()->toNormalizedRange()); 209 pasteboardHelperInstance()->writeClipboardContents(clipboard, m_webView); 210 } 211 #endif 212 213 if (!targetFrame->editor()->hasComposition()) 214 return; 215 216 unsigned start; 217 unsigned end; 218 if (!targetFrame->editor()->getCompositionSelection(start, end)) { 219 // gtk_im_context_reset() clears the composition for us. 220 gtk_im_context_reset(priv->imContext); 221 targetFrame->editor()->confirmCompositionWithoutDisturbingSelection(); 222 } 223 } 224 225 void EditorClient::didEndEditing() 226 { 227 notImplemented(); 228 } 229 230 void EditorClient::didWriteSelectionToPasteboard() 231 { 232 notImplemented(); 233 } 234 235 void EditorClient::didSetSelectionTypesForPasteboard() 236 { 237 notImplemented(); 238 } 239 240 bool EditorClient::isEditable() 241 { 242 return webkit_web_view_get_editable(m_webView); 243 } 244 245 void EditorClient::registerCommandForUndo(WTF::PassRefPtr<WebCore::EditCommand> command) 246 { 247 if (undoStack.size() == maximumUndoStackDepth) 248 undoStack.removeFirst(); 249 if (!m_isInRedo) 250 redoStack.clear(); 251 undoStack.append(command); 252 } 253 254 void EditorClient::registerCommandForRedo(WTF::PassRefPtr<WebCore::EditCommand> command) 255 { 256 redoStack.append(command); 257 } 258 259 void EditorClient::clearUndoRedoOperations() 260 { 261 undoStack.clear(); 262 redoStack.clear(); 263 } 264 265 bool EditorClient::canUndo() const 266 { 267 return !undoStack.isEmpty(); 268 } 269 270 bool EditorClient::canRedo() const 271 { 272 return !redoStack.isEmpty(); 273 } 274 275 void EditorClient::undo() 276 { 277 if (canUndo()) { 278 RefPtr<WebCore::EditCommand> command(*(--undoStack.end())); 279 undoStack.remove(--undoStack.end()); 280 // unapply will call us back to push this command onto the redo stack. 281 command->unapply(); 282 } 283 } 284 285 void EditorClient::redo() 286 { 287 if (canRedo()) { 288 RefPtr<WebCore::EditCommand> command(*(--redoStack.end())); 289 redoStack.remove(--redoStack.end()); 290 291 ASSERT(!m_isInRedo); 292 m_isInRedo = true; 293 // reapply will call us back to push this command onto the undo stack. 294 command->reapply(); 295 m_isInRedo = false; 296 } 297 } 298 299 bool EditorClient::shouldInsertNode(Node*, Range*, EditorInsertAction) 300 { 301 notImplemented(); 302 return true; 303 } 304 305 void EditorClient::pageDestroyed() 306 { 307 delete this; 308 } 309 310 bool EditorClient::smartInsertDeleteEnabled() 311 { 312 notImplemented(); 313 return false; 314 } 315 316 bool EditorClient::isSelectTrailingWhitespaceEnabled() 317 { 318 notImplemented(); 319 return false; 320 } 321 322 void EditorClient::toggleContinuousSpellChecking() 323 { 324 WebKitWebSettings* settings = webkit_web_view_get_settings(m_webView); 325 326 gboolean enabled; 327 g_object_get(settings, "enable-spell-checking", &enabled, NULL); 328 329 g_object_set(settings, "enable-spell-checking", !enabled, NULL); 330 } 331 332 void EditorClient::toggleGrammarChecking() 333 { 334 } 335 336 static const unsigned CtrlKey = 1 << 0; 337 static const unsigned AltKey = 1 << 1; 338 static const unsigned ShiftKey = 1 << 2; 339 340 struct KeyDownEntry { 341 unsigned virtualKey; 342 unsigned modifiers; 343 const char* name; 344 }; 345 346 struct KeyPressEntry { 347 unsigned charCode; 348 unsigned modifiers; 349 const char* name; 350 }; 351 352 static const KeyDownEntry keyDownEntries[] = { 353 { VK_LEFT, 0, "MoveLeft" }, 354 { VK_LEFT, ShiftKey, "MoveLeftAndModifySelection" }, 355 { VK_LEFT, CtrlKey, "MoveWordLeft" }, 356 { VK_LEFT, CtrlKey | ShiftKey, "MoveWordLeftAndModifySelection" }, 357 { VK_RIGHT, 0, "MoveRight" }, 358 { VK_RIGHT, ShiftKey, "MoveRightAndModifySelection" }, 359 { VK_RIGHT, CtrlKey, "MoveWordRight" }, 360 { VK_RIGHT, CtrlKey | ShiftKey, "MoveWordRightAndModifySelection" }, 361 { VK_UP, 0, "MoveUp" }, 362 { VK_UP, ShiftKey, "MoveUpAndModifySelection" }, 363 { VK_PRIOR, ShiftKey, "MovePageUpAndModifySelection" }, 364 { VK_DOWN, 0, "MoveDown" }, 365 { VK_DOWN, ShiftKey, "MoveDownAndModifySelection" }, 366 { VK_NEXT, ShiftKey, "MovePageDownAndModifySelection" }, 367 { VK_PRIOR, 0, "MovePageUp" }, 368 { VK_NEXT, 0, "MovePageDown" }, 369 { VK_HOME, 0, "MoveToBeginningOfLine" }, 370 { VK_HOME, ShiftKey, "MoveToBeginningOfLineAndModifySelection" }, 371 { VK_HOME, CtrlKey, "MoveToBeginningOfDocument" }, 372 { VK_HOME, CtrlKey | ShiftKey, "MoveToBeginningOfDocumentAndModifySelection" }, 373 374 { VK_END, 0, "MoveToEndOfLine" }, 375 { VK_END, ShiftKey, "MoveToEndOfLineAndModifySelection" }, 376 { VK_END, CtrlKey, "MoveToEndOfDocument" }, 377 { VK_END, CtrlKey | ShiftKey, "MoveToEndOfDocumentAndModifySelection" }, 378 379 { VK_BACK, 0, "DeleteBackward" }, 380 { VK_BACK, ShiftKey, "DeleteBackward" }, 381 { VK_DELETE, 0, "DeleteForward" }, 382 { VK_BACK, CtrlKey, "DeleteWordBackward" }, 383 { VK_DELETE, CtrlKey, "DeleteWordForward" }, 384 385 { 'B', CtrlKey, "ToggleBold" }, 386 { 'I', CtrlKey, "ToggleItalic" }, 387 388 { VK_ESCAPE, 0, "Cancel" }, 389 { VK_OEM_PERIOD, CtrlKey, "Cancel" }, 390 { VK_TAB, 0, "InsertTab" }, 391 { VK_TAB, ShiftKey, "InsertBacktab" }, 392 { VK_RETURN, 0, "InsertNewline" }, 393 { VK_RETURN, CtrlKey, "InsertNewline" }, 394 { VK_RETURN, AltKey, "InsertNewline" }, 395 { VK_RETURN, AltKey | ShiftKey, "InsertNewline" }, 396 }; 397 398 static const KeyPressEntry keyPressEntries[] = { 399 { '\t', 0, "InsertTab" }, 400 { '\t', ShiftKey, "InsertBacktab" }, 401 { '\r', 0, "InsertNewline" }, 402 { '\r', CtrlKey, "InsertNewline" }, 403 { '\r', AltKey, "InsertNewline" }, 404 { '\r', AltKey | ShiftKey, "InsertNewline" }, 405 }; 406 407 static const char* interpretEditorCommandKeyEvent(const KeyboardEvent* evt) 408 { 409 ASSERT(evt->type() == eventNames().keydownEvent || evt->type() == eventNames().keypressEvent); 410 411 static HashMap<int, const char*>* keyDownCommandsMap = 0; 412 static HashMap<int, const char*>* keyPressCommandsMap = 0; 413 414 if (!keyDownCommandsMap) { 415 keyDownCommandsMap = new HashMap<int, const char*>; 416 keyPressCommandsMap = new HashMap<int, const char*>; 417 418 for (unsigned i = 0; i < G_N_ELEMENTS(keyDownEntries); i++) 419 keyDownCommandsMap->set(keyDownEntries[i].modifiers << 16 | keyDownEntries[i].virtualKey, keyDownEntries[i].name); 420 421 for (unsigned i = 0; i < G_N_ELEMENTS(keyPressEntries); i++) 422 keyPressCommandsMap->set(keyPressEntries[i].modifiers << 16 | keyPressEntries[i].charCode, keyPressEntries[i].name); 423 } 424 425 unsigned modifiers = 0; 426 if (evt->shiftKey()) 427 modifiers |= ShiftKey; 428 if (evt->altKey()) 429 modifiers |= AltKey; 430 if (evt->ctrlKey()) 431 modifiers |= CtrlKey; 432 433 if (evt->type() == eventNames().keydownEvent) { 434 int mapKey = modifiers << 16 | evt->keyCode(); 435 return mapKey ? keyDownCommandsMap->get(mapKey) : 0; 436 } 437 438 int mapKey = modifiers << 16 | evt->charCode(); 439 return mapKey ? keyPressCommandsMap->get(mapKey) : 0; 440 } 441 442 void EditorClient::handleKeyboardEvent(KeyboardEvent* event) 443 { 444 Node* node = event->target()->toNode(); 445 ASSERT(node); 446 Frame* frame = node->document()->frame(); 447 ASSERT(frame); 448 449 const PlatformKeyboardEvent* platformEvent = event->keyEvent(); 450 if (!platformEvent) 451 return; 452 453 // Don't allow editor commands or text insertion for nodes that 454 // cannot edit, unless we are in caret mode. 455 if (!frame->editor()->canEdit() && !(frame->settings() && frame->settings()->caretBrowsingEnabled())) 456 return; 457 458 const gchar* editorCommandString = interpretEditorCommandKeyEvent(event); 459 if (editorCommandString) { 460 Editor::Command command = frame->editor()->command(editorCommandString); 461 462 // On editor commands from key down events, we only want to let the event bubble up to 463 // the DOM if it inserts text. If it doesn't insert text (e.g. Tab that changes focus) 464 // we just want WebKit to handle it immediately without a DOM event. 465 if (platformEvent->type() == PlatformKeyboardEvent::RawKeyDown) { 466 if (!command.isTextInsertion() && command.execute(event)) 467 event->setDefaultHandled(); 468 469 return; 470 } else if (command.execute(event)) { 471 event->setDefaultHandled(); 472 return; 473 } 474 } 475 476 // This is just a normal text insertion, so wait to execute the insertion 477 // until a keypress event happens. This will ensure that the insertion will not 478 // be reflected in the contents of the field until the keyup DOM event. 479 if (event->type() == eventNames().keypressEvent) { 480 481 if (pendingComposition) { 482 String compositionString = String::fromUTF8(pendingComposition); 483 frame->editor()->confirmComposition(compositionString); 484 485 clearPendingIMData(); 486 event->setDefaultHandled(); 487 488 } else if (pendingPreedit) { 489 String preeditString = String::fromUTF8(pendingPreedit); 490 491 // Don't use an empty preedit as it will destroy the current 492 // selection, even if the composition is cancelled or fails later on. 493 if (!preeditString.isEmpty()) { 494 Vector<CompositionUnderline> underlines; 495 underlines.append(CompositionUnderline(0, preeditString.length(), Color(0, 0, 0), false)); 496 frame->editor()->setComposition(preeditString, underlines, 0, 0); 497 } 498 499 clearPendingIMData(); 500 event->setDefaultHandled(); 501 502 } else { 503 // Don't insert null or control characters as they can result in unexpected behaviour 504 if (event->charCode() < ' ') 505 return; 506 507 // Don't insert anything if a modifier is pressed 508 if (platformEvent->ctrlKey() || platformEvent->altKey()) 509 return; 510 511 if (frame->editor()->insertText(platformEvent->text(), event)) 512 event->setDefaultHandled(); 513 } 514 } 515 } 516 517 void EditorClient::handleInputMethodKeydown(KeyboardEvent* event) 518 { 519 Frame* targetFrame = core(m_webView)->focusController()->focusedOrMainFrame(); 520 if (!targetFrame || !targetFrame->editor()->canEdit()) 521 return; 522 523 // TODO: We need to decide which filtered keystrokes should be treated as IM 524 // events and which should not. 525 WebKitWebViewPrivate* priv = m_webView->priv; 526 gtk_im_context_filter_keypress(priv->imContext, event->keyEvent()->gdkEventKey()); 527 } 528 529 EditorClient::EditorClient(WebKitWebView* webView) 530 : m_isInRedo(false) 531 , m_webView(webView) 532 { 533 WebKitWebViewPrivate* priv = m_webView->priv; 534 g_signal_connect(priv->imContext, "commit", G_CALLBACK(imContextCommitted), this); 535 g_signal_connect(priv->imContext, "preedit-changed", G_CALLBACK(imContextPreeditChanged), this); 536 } 537 538 EditorClient::~EditorClient() 539 { 540 WebKitWebViewPrivate* priv = m_webView->priv; 541 g_signal_handlers_disconnect_by_func(priv->imContext, (gpointer)imContextCommitted, this); 542 g_signal_handlers_disconnect_by_func(priv->imContext, (gpointer)imContextPreeditChanged, this); 543 } 544 545 void EditorClient::textFieldDidBeginEditing(Element*) 546 { 547 } 548 549 void EditorClient::textFieldDidEndEditing(Element*) 550 { 551 } 552 553 void EditorClient::textDidChangeInTextField(Element*) 554 { 555 } 556 557 bool EditorClient::doTextFieldCommandFromEvent(Element*, KeyboardEvent*) 558 { 559 return false; 560 } 561 562 void EditorClient::textWillBeDeletedInTextField(Element*) 563 { 564 notImplemented(); 565 } 566 567 void EditorClient::textDidChangeInTextArea(Element*) 568 { 569 notImplemented(); 570 } 571 572 void EditorClient::ignoreWordInSpellDocument(const String& text) 573 { 574 GSList* langs = webkit_web_settings_get_spell_languages(m_webView); 575 576 for (; langs; langs = langs->next) { 577 SpellLanguage* lang = static_cast<SpellLanguage*>(langs->data); 578 579 enchant_dict_add_to_session(lang->speller, text.utf8().data(), -1); 580 } 581 } 582 583 void EditorClient::learnWord(const String& text) 584 { 585 GSList* langs = webkit_web_settings_get_spell_languages(m_webView); 586 587 for (; langs; langs = langs->next) { 588 SpellLanguage* lang = static_cast<SpellLanguage*>(langs->data); 589 590 enchant_dict_add_to_personal(lang->speller, text.utf8().data(), -1); 591 } 592 } 593 594 void EditorClient::checkSpellingOfString(const UChar* text, int length, int* misspellingLocation, int* misspellingLength) 595 { 596 GSList* langs = webkit_web_settings_get_spell_languages(m_webView); 597 if (!langs) 598 return; 599 600 gchar* ctext = g_utf16_to_utf8(const_cast<gunichar2*>(text), length, 0, 0, 0); 601 int utflen = g_utf8_strlen(ctext, -1); 602 603 PangoLanguage* language = pango_language_get_default(); 604 PangoLogAttr* attrs = g_new(PangoLogAttr, utflen+1); 605 606 // pango_get_log_attrs uses an aditional position at the end of the text. 607 pango_get_log_attrs(ctext, -1, -1, language, attrs, utflen+1); 608 609 for (int i = 0; i < length+1; i++) { 610 // We go through each character until we find an is_word_start, 611 // then we get into an inner loop to find the is_word_end corresponding 612 // to it. 613 if (attrs[i].is_word_start) { 614 int start = i; 615 int end = i; 616 int wordLength; 617 618 while (attrs[end].is_word_end < 1) 619 end++; 620 621 wordLength = end - start; 622 // Set the iterator to be at the current word end, so we don't 623 // check characters twice. 624 i = end; 625 626 for (; langs; langs = langs->next) { 627 SpellLanguage* lang = static_cast<SpellLanguage*>(langs->data); 628 gchar* cstart = g_utf8_offset_to_pointer(ctext, start); 629 gint bytes = static_cast<gint>(g_utf8_offset_to_pointer(ctext, end) - cstart); 630 gchar* word = g_new0(gchar, bytes+1); 631 int result; 632 633 g_utf8_strncpy(word, cstart, end - start); 634 635 result = enchant_dict_check(lang->speller, word, -1); 636 g_free(word); 637 if (result) { 638 *misspellingLocation = start; 639 *misspellingLength = wordLength; 640 } else { 641 // Stop checking, this word is ok in at least one dict. 642 *misspellingLocation = -1; 643 *misspellingLength = 0; 644 break; 645 } 646 } 647 } 648 } 649 650 g_free(attrs); 651 g_free(ctext); 652 } 653 654 String EditorClient::getAutoCorrectSuggestionForMisspelledWord(const String& inputWord) 655 { 656 // This method can be implemented using customized algorithms for the particular browser. 657 // Currently, it computes an empty string. 658 return String(); 659 } 660 661 void EditorClient::checkGrammarOfString(const UChar*, int, Vector<GrammarDetail>&, int*, int*) 662 { 663 notImplemented(); 664 } 665 666 void EditorClient::updateSpellingUIWithGrammarString(const String&, const GrammarDetail&) 667 { 668 notImplemented(); 669 } 670 671 void EditorClient::updateSpellingUIWithMisspelledWord(const String&) 672 { 673 notImplemented(); 674 } 675 676 void EditorClient::showSpellingUI(bool) 677 { 678 notImplemented(); 679 } 680 681 bool EditorClient::spellingUIIsShowing() 682 { 683 notImplemented(); 684 return false; 685 } 686 687 void EditorClient::getGuessesForWord(const String& word, WTF::Vector<String>& guesses) 688 { 689 GSList* langs = webkit_web_settings_get_spell_languages(m_webView); 690 guesses.clear(); 691 692 for (; langs; langs = langs->next) { 693 size_t numberOfSuggestions; 694 size_t i; 695 696 SpellLanguage* lang = static_cast<SpellLanguage*>(langs->data); 697 gchar** suggestions = enchant_dict_suggest(lang->speller, word.utf8().data(), -1, &numberOfSuggestions); 698 699 for (i = 0; i < numberOfSuggestions && i < 10; i++) 700 guesses.append(String::fromUTF8(suggestions[i])); 701 702 if (numberOfSuggestions > 0) 703 enchant_dict_free_suggestions(lang->speller, suggestions); 704 } 705 } 706 707 } 708