// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/renderer/spellchecker/spellcheck_provider.h" #include "base/command_line.h" #include "base/metrics/histogram.h" #include "chrome/common/chrome_switches.h" #include "chrome/common/spellcheck_marker.h" #include "chrome/common/spellcheck_messages.h" #include "chrome/common/spellcheck_result.h" #include "chrome/renderer/spellchecker/spellcheck.h" #include "chrome/renderer/spellchecker/spellcheck_language.h" #include "content/public/renderer/render_view.h" #include "third_party/WebKit/public/platform/WebVector.h" #include "third_party/WebKit/public/web/WebDocument.h" #include "third_party/WebKit/public/web/WebElement.h" #include "third_party/WebKit/public/web/WebFrame.h" #include "third_party/WebKit/public/web/WebTextCheckingCompletion.h" #include "third_party/WebKit/public/web/WebTextCheckingResult.h" #include "third_party/WebKit/public/web/WebTextDecorationType.h" #include "third_party/WebKit/public/web/WebView.h" using blink::WebElement; using blink::WebFrame; using blink::WebString; using blink::WebTextCheckingCompletion; using blink::WebTextCheckingResult; using blink::WebTextDecorationType; using blink::WebVector; static_assert(int(blink::WebTextDecorationTypeSpelling) == int(SpellCheckResult::SPELLING), "mismatching enums"); static_assert(int(blink::WebTextDecorationTypeGrammar) == int(SpellCheckResult::GRAMMAR), "mismatching enums"); static_assert(int(blink::WebTextDecorationTypeInvisibleSpellcheck) == int(SpellCheckResult::INVISIBLE), "mismatching enums"); SpellCheckProvider::SpellCheckProvider( content::RenderView* render_view, SpellCheck* spellcheck) : content::RenderViewObserver(render_view), content::RenderViewObserverTracker(render_view), spelling_panel_visible_(false), spellcheck_(spellcheck) { DCHECK(spellcheck_); if (render_view) { // NULL in unit tests. render_view->GetWebView()->setSpellCheckClient(this); EnableSpellcheck(spellcheck_->IsSpellcheckEnabled()); } } SpellCheckProvider::~SpellCheckProvider() { } void SpellCheckProvider::RequestTextChecking( const base::string16& text, WebTextCheckingCompletion* completion, const std::vector& markers) { // Ignore invalid requests. if (text.empty() || !HasWordCharacters(text, 0)) { completion->didCancelCheckingText(); return; } // Try to satisfy check from cache. if (SatisfyRequestFromCache(text, completion)) return; // Send this text to a browser. A browser checks the user profile and send // this text to the Spelling service only if a user enables this feature. last_request_.clear(); last_results_.assign(blink::WebVector()); #if defined(USE_BROWSER_SPELLCHECKER) // Text check (unified request for grammar and spell check) is only // available for browser process, so we ask the system spellchecker // over IPC or return an empty result if the checker is not // available. Send(new SpellCheckHostMsg_RequestTextCheck( routing_id(), text_check_completions_.Add(completion), text, markers)); #else Send(new SpellCheckHostMsg_CallSpellingService( routing_id(), text_check_completions_.Add(completion), base::string16(text), markers)); #endif // !USE_BROWSER_SPELLCHECKER } bool SpellCheckProvider::OnMessageReceived(const IPC::Message& message) { bool handled = true; IPC_BEGIN_MESSAGE_MAP(SpellCheckProvider, message) #if !defined(USE_BROWSER_SPELLCHECKER) IPC_MESSAGE_HANDLER(SpellCheckMsg_RespondSpellingService, OnRespondSpellingService) #endif #if defined(USE_BROWSER_SPELLCHECKER) IPC_MESSAGE_HANDLER(SpellCheckMsg_AdvanceToNextMisspelling, OnAdvanceToNextMisspelling) IPC_MESSAGE_HANDLER(SpellCheckMsg_RespondTextCheck, OnRespondTextCheck) IPC_MESSAGE_HANDLER(SpellCheckMsg_ToggleSpellPanel, OnToggleSpellPanel) #endif IPC_MESSAGE_UNHANDLED(handled = false) IPC_END_MESSAGE_MAP() return handled; } void SpellCheckProvider::FocusedNodeChanged(const blink::WebNode& unused) { #if defined(USE_BROWSER_SPELLCHECKER) WebFrame* frame = render_view()->GetWebView()->focusedFrame(); WebElement element = frame->document().isNull() ? WebElement() : frame->document().focusedElement(); bool enabled = !element.isNull() && element.isEditable(); bool checked = enabled && frame->isContinuousSpellCheckingEnabled(); Send(new SpellCheckHostMsg_ToggleSpellCheck(routing_id(), enabled, checked)); #endif // USE_BROWSER_SPELLCHECKER } void SpellCheckProvider::spellCheck( const WebString& text, int& offset, int& length, WebVector* optional_suggestions) { base::string16 word(text); std::vector suggestions; const int kWordStart = 0; spellcheck_->SpellCheckWord( word.c_str(), kWordStart, word.size(), routing_id(), &offset, &length, optional_suggestions ? & suggestions : NULL); if (optional_suggestions) { *optional_suggestions = suggestions; UMA_HISTOGRAM_COUNTS("SpellCheck.api.check.suggestions", word.size()); } else { UMA_HISTOGRAM_COUNTS("SpellCheck.api.check", word.size()); // If optional_suggestions is not requested, the API is called // for marking. So we use this for counting markable words. Send(new SpellCheckHostMsg_NotifyChecked(routing_id(), word, 0 < length)); } } void SpellCheckProvider::checkTextOfParagraph( const blink::WebString& text, blink::WebTextCheckingTypeMask mask, blink::WebVector* results) { if (!results) return; if (!(mask & blink::WebTextCheckingTypeSpelling)) return; // TODO(groby): As far as I can tell, this method is never invoked. // UMA results seem to support that. Investigate, clean up if true. NOTREACHED(); spellcheck_->SpellCheckParagraph(text, results); UMA_HISTOGRAM_COUNTS("SpellCheck.api.paragraph", text.length()); } void SpellCheckProvider::requestCheckingOfText( const WebString& text, const WebVector& markers, const WebVector& marker_offsets, WebTextCheckingCompletion* completion) { std::vector spellcheck_markers; for (size_t i = 0; i < markers.size(); ++i) { spellcheck_markers.push_back( SpellCheckMarker(markers[i], marker_offsets[i])); } RequestTextChecking(text, completion, spellcheck_markers); UMA_HISTOGRAM_COUNTS("SpellCheck.api.async", text.length()); } void SpellCheckProvider::showSpellingUI(bool show) { #if defined(USE_BROWSER_SPELLCHECKER) UMA_HISTOGRAM_BOOLEAN("SpellCheck.api.showUI", show); Send(new SpellCheckHostMsg_ShowSpellingPanel(routing_id(), show)); #endif } bool SpellCheckProvider::isShowingSpellingUI() { return spelling_panel_visible_; } void SpellCheckProvider::updateSpellingUIWithMisspelledWord( const WebString& word) { #if defined(USE_BROWSER_SPELLCHECKER) Send(new SpellCheckHostMsg_UpdateSpellingPanelWithMisspelledWord(routing_id(), word)); #endif } #if !defined(USE_BROWSER_SPELLCHECKER) void SpellCheckProvider::OnRespondSpellingService( int identifier, bool succeeded, const base::string16& line, const std::vector& results) { WebTextCheckingCompletion* completion = text_check_completions_.Lookup(identifier); if (!completion) return; text_check_completions_.Remove(identifier); // If |succeeded| is false, we use local spellcheck as a fallback. if (!succeeded) { spellcheck_->RequestTextChecking(line, completion); return; } // Double-check the returned spellchecking results with our spellchecker to // visualize the differences between ours and the on-line spellchecker. blink::WebVector textcheck_results; spellcheck_->CreateTextCheckingResults(SpellCheck::USE_NATIVE_CHECKER, 0, line, results, &textcheck_results); completion->didFinishCheckingText(textcheck_results); // Cache the request and the converted results. last_request_ = line; last_results_.swap(textcheck_results); } #endif bool SpellCheckProvider::HasWordCharacters( const base::string16& text, int index) const { const base::char16* data = text.data(); int length = text.length(); while (index < length) { uint32_t code = 0; U16_NEXT(data, index, length, code); UErrorCode error = U_ZERO_ERROR; if (uscript_getScript(code, &error) != USCRIPT_COMMON) return true; } return false; } #if defined(USE_BROWSER_SPELLCHECKER) void SpellCheckProvider::OnAdvanceToNextMisspelling() { if (!render_view()->GetWebView()) return; render_view()->GetWebView()->focusedFrame()->executeCommand( WebString::fromUTF8("AdvanceToNextMisspelling")); } void SpellCheckProvider::OnRespondTextCheck( int identifier, const base::string16& line, const std::vector& results) { // TODO(groby): Unify with SpellCheckProvider::OnRespondSpellingService DCHECK(spellcheck_); WebTextCheckingCompletion* completion = text_check_completions_.Lookup(identifier); if (!completion) return; text_check_completions_.Remove(identifier); blink::WebVector textcheck_results; spellcheck_->CreateTextCheckingResults(SpellCheck::DO_NOT_MODIFY, 0, line, results, &textcheck_results); completion->didFinishCheckingText(textcheck_results); // TODO(groby): Add request caching once OSX reports back original request. // (cf. SpellCheckProvider::OnRespondSpellingService) // Cache the request and the converted results. } void SpellCheckProvider::OnToggleSpellPanel(bool is_currently_visible) { if (!render_view()->GetWebView()) return; // We need to tell the webView whether the spelling panel is visible or not so // that it won't need to make ipc calls later. spelling_panel_visible_ = is_currently_visible; render_view()->GetWebView()->focusedFrame()->executeCommand( WebString::fromUTF8("ToggleSpellPanel")); } #endif void SpellCheckProvider::EnableSpellcheck(bool enable) { if (!render_view()->GetWebView()) return; WebFrame* frame = render_view()->GetWebView()->focusedFrame(); frame->enableContinuousSpellChecking(enable); if (!enable) frame->removeSpellingMarkers(); } bool SpellCheckProvider::SatisfyRequestFromCache( const base::string16& text, WebTextCheckingCompletion* completion) { size_t last_length = last_request_.length(); // Send back the |last_results_| if the |last_request_| is a substring of // |text| and |text| does not have more words to check. Provider cannot cancel // the spellcheck request here, because WebKit might have discarded the // previous spellcheck results and erased the spelling markers in response to // the user editing the text. base::string16 request(text); size_t text_length = request.length(); if (text_length >= last_length && !request.compare(0, last_length, last_request_)) { if (text_length == last_length || !HasWordCharacters(text, last_length)) { completion->didFinishCheckingText(last_results_); return true; } int code = 0; int length = static_cast(text_length); U16_PREV(text.data(), 0, length, code); UErrorCode error = U_ZERO_ERROR; if (uscript_getScript(code, &error) != USCRIPT_COMMON) { completion->didCancelCheckingText(); return true; } } // Create a subset of the cached results and return it if the given text is a // substring of the cached text. if (text_length < last_length && !last_request_.compare(0, text_length, request)) { size_t result_size = 0; for (size_t i = 0; i < last_results_.size(); ++i) { size_t start = last_results_[i].location; size_t end = start + last_results_[i].length; if (start <= text_length && end <= text_length) ++result_size; } if (result_size > 0) { blink::WebVector results(result_size); for (size_t i = 0; i < result_size; ++i) { results[i].decoration = last_results_[i].decoration; results[i].location = last_results_[i].location; results[i].length = last_results_[i].length; results[i].replacement = last_results_[i].replacement; } completion->didFinishCheckingText(results); return true; } } return false; }