1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/renderer/spellchecker/spellcheck_provider.h"
6
7#include "base/command_line.h"
8#include "base/metrics/histogram.h"
9#include "chrome/common/chrome_switches.h"
10#include "chrome/common/spellcheck_marker.h"
11#include "chrome/common/spellcheck_messages.h"
12#include "chrome/common/spellcheck_result.h"
13#include "chrome/renderer/spellchecker/spellcheck.h"
14#include "content/public/renderer/render_view.h"
15#include "third_party/WebKit/public/platform/WebVector.h"
16#include "third_party/WebKit/public/web/WebElement.h"
17#include "third_party/WebKit/public/web/WebFrame.h"
18#include "third_party/WebKit/public/web/WebTextCheckingCompletion.h"
19#include "third_party/WebKit/public/web/WebTextCheckingResult.h"
20#include "third_party/WebKit/public/web/WebTextDecorationType.h"
21#include "third_party/WebKit/public/web/WebView.h"
22
23using blink::WebFrame;
24using blink::WebString;
25using blink::WebTextCheckingCompletion;
26using blink::WebTextCheckingResult;
27using blink::WebTextDecorationType;
28using blink::WebVector;
29
30COMPILE_ASSERT(int(blink::WebTextDecorationTypeSpelling) ==
31               int(SpellCheckResult::SPELLING), mismatching_enums);
32COMPILE_ASSERT(int(blink::WebTextDecorationTypeGrammar) ==
33               int(SpellCheckResult::GRAMMAR), mismatching_enums);
34COMPILE_ASSERT(int(blink::WebTextDecorationTypeInvisibleSpellcheck) ==
35               int(SpellCheckResult::INVISIBLE), mismatching_enums);
36
37SpellCheckProvider::SpellCheckProvider(
38    content::RenderView* render_view,
39    SpellCheck* spellcheck)
40    : content::RenderViewObserver(render_view),
41      content::RenderViewObserverTracker<SpellCheckProvider>(render_view),
42      spelling_panel_visible_(false),
43      spellcheck_(spellcheck) {
44  DCHECK(spellcheck_);
45  if (render_view) {  // NULL in unit tests.
46    render_view->GetWebView()->setSpellCheckClient(this);
47    EnableSpellcheck(spellcheck_->is_spellcheck_enabled());
48  }
49}
50
51SpellCheckProvider::~SpellCheckProvider() {
52}
53
54void SpellCheckProvider::RequestTextChecking(
55    const base::string16& text,
56    WebTextCheckingCompletion* completion,
57    const std::vector<SpellCheckMarker>& markers) {
58  // Ignore invalid requests.
59  if (text.empty() || !HasWordCharacters(text, 0)) {
60    completion->didCancelCheckingText();
61    return;
62  }
63
64  // Try to satisfy check from cache.
65  if (SatisfyRequestFromCache(text, completion))
66    return;
67
68  // Send this text to a browser. A browser checks the user profile and send
69  // this text to the Spelling service only if a user enables this feature.
70  last_request_.clear();
71  last_results_.assign(blink::WebVector<blink::WebTextCheckingResult>());
72
73#if defined(OS_MACOSX)
74  // Text check (unified request for grammar and spell check) is only
75  // available for browser process, so we ask the system spellchecker
76  // over IPC or return an empty result if the checker is not
77  // available.
78  Send(new SpellCheckHostMsg_RequestTextCheck(
79      routing_id(),
80      text_check_completions_.Add(completion),
81      text,
82      markers));
83#else
84  Send(new SpellCheckHostMsg_CallSpellingService(
85      routing_id(),
86      text_check_completions_.Add(completion),
87      base::string16(text),
88      markers));
89#endif  // !OS_MACOSX
90}
91
92bool SpellCheckProvider::OnMessageReceived(const IPC::Message& message) {
93  bool handled = true;
94  IPC_BEGIN_MESSAGE_MAP(SpellCheckProvider, message)
95#if !defined(OS_MACOSX)
96    IPC_MESSAGE_HANDLER(SpellCheckMsg_RespondSpellingService,
97                        OnRespondSpellingService)
98#endif
99#if defined(OS_MACOSX)
100    IPC_MESSAGE_HANDLER(SpellCheckMsg_AdvanceToNextMisspelling,
101                        OnAdvanceToNextMisspelling)
102    IPC_MESSAGE_HANDLER(SpellCheckMsg_RespondTextCheck, OnRespondTextCheck)
103    IPC_MESSAGE_HANDLER(SpellCheckMsg_ToggleSpellPanel, OnToggleSpellPanel)
104#endif
105    IPC_MESSAGE_UNHANDLED(handled = false)
106  IPC_END_MESSAGE_MAP()
107  return handled;
108}
109
110void SpellCheckProvider::FocusedNodeChanged(const blink::WebNode& unused) {
111#if defined(OS_MACOSX)
112  bool enabled = false;
113  blink::WebElement element = render_view()->GetFocusedElement();
114  if (!element.isNull())
115    enabled = render_view()->IsEditableNode(element);
116
117  bool checked = false;
118  if (enabled && render_view()->GetWebView()) {
119    WebFrame* frame = render_view()->GetWebView()->focusedFrame();
120    if (frame->isContinuousSpellCheckingEnabled())
121      checked = true;
122  }
123
124  Send(new SpellCheckHostMsg_ToggleSpellCheck(routing_id(), enabled, checked));
125#endif  // OS_MACOSX
126}
127
128void SpellCheckProvider::spellCheck(
129    const WebString& text,
130    int& offset,
131    int& length,
132    WebVector<WebString>* optional_suggestions) {
133  base::string16 word(text);
134  std::vector<base::string16> suggestions;
135  spellcheck_->SpellCheckWord(
136      word.c_str(), word.size(), routing_id(),
137      &offset, &length, optional_suggestions ? & suggestions : NULL);
138  if (optional_suggestions) {
139    *optional_suggestions = suggestions;
140    UMA_HISTOGRAM_COUNTS("SpellCheck.api.check.suggestions", word.size());
141  } else {
142    UMA_HISTOGRAM_COUNTS("SpellCheck.api.check", word.size());
143    // If optional_suggestions is not requested, the API is called
144    // for marking.  So we use this for counting markable words.
145    Send(new SpellCheckHostMsg_NotifyChecked(routing_id(), word, 0 < length));
146  }
147}
148
149void SpellCheckProvider::checkTextOfParagraph(
150    const blink::WebString& text,
151    blink::WebTextCheckingTypeMask mask,
152    blink::WebVector<blink::WebTextCheckingResult>* results) {
153  if (!results)
154    return;
155
156  if (!(mask & blink::WebTextCheckingTypeSpelling))
157    return;
158
159  // TODO(groby): As far as I can tell, this method is never invoked.
160  // UMA results seem to support that. Investigate, clean up if true.
161  NOTREACHED();
162  spellcheck_->SpellCheckParagraph(text, results);
163  UMA_HISTOGRAM_COUNTS("SpellCheck.api.paragraph", text.length());
164}
165
166void SpellCheckProvider::requestCheckingOfText(
167    const WebString& text,
168    const WebVector<uint32>& markers,
169    const WebVector<unsigned>& marker_offsets,
170    WebTextCheckingCompletion* completion) {
171  std::vector<SpellCheckMarker> spellcheck_markers;
172  for (size_t i = 0; i < markers.size(); ++i) {
173    spellcheck_markers.push_back(
174        SpellCheckMarker(markers[i], marker_offsets[i]));
175  }
176  RequestTextChecking(text, completion, spellcheck_markers);
177  UMA_HISTOGRAM_COUNTS("SpellCheck.api.async", text.length());
178}
179
180WebString SpellCheckProvider::autoCorrectWord(const WebString& word) {
181  const CommandLine& command_line = *CommandLine::ForCurrentProcess();
182  if (command_line.HasSwitch(switches::kEnableSpellingAutoCorrect)) {
183    UMA_HISTOGRAM_COUNTS("SpellCheck.api.autocorrect", word.length());
184    return spellcheck_->GetAutoCorrectionWord(word, routing_id());
185  }
186  return base::string16();
187}
188
189void SpellCheckProvider::showSpellingUI(bool show) {
190#if defined(OS_MACOSX)
191  UMA_HISTOGRAM_BOOLEAN("SpellCheck.api.showUI", show);
192  Send(new SpellCheckHostMsg_ShowSpellingPanel(routing_id(), show));
193#endif
194}
195
196bool SpellCheckProvider::isShowingSpellingUI() {
197  return spelling_panel_visible_;
198}
199
200void SpellCheckProvider::updateSpellingUIWithMisspelledWord(
201    const WebString& word) {
202#if defined(OS_MACOSX)
203  Send(new SpellCheckHostMsg_UpdateSpellingPanelWithMisspelledWord(routing_id(),
204                                                                   word));
205#endif
206}
207
208#if !defined(OS_MACOSX)
209void SpellCheckProvider::OnRespondSpellingService(
210    int identifier,
211    bool succeeded,
212    const base::string16& line,
213    const std::vector<SpellCheckResult>& results) {
214  WebTextCheckingCompletion* completion =
215      text_check_completions_.Lookup(identifier);
216  if (!completion)
217    return;
218  text_check_completions_.Remove(identifier);
219
220  // If |succeeded| is false, we use local spellcheck as a fallback.
221  if (!succeeded) {
222    spellcheck_->RequestTextChecking(line, completion);
223    return;
224  }
225
226  // Double-check the returned spellchecking results with our spellchecker to
227  // visualize the differences between ours and the on-line spellchecker.
228  blink::WebVector<blink::WebTextCheckingResult> textcheck_results;
229  spellcheck_->CreateTextCheckingResults(SpellCheck::USE_NATIVE_CHECKER,
230                                         0,
231                                         line,
232                                         results,
233                                         &textcheck_results);
234  completion->didFinishCheckingText(textcheck_results);
235
236  // Cache the request and the converted results.
237  last_request_ = line;
238  last_results_.swap(textcheck_results);
239}
240#endif
241
242bool SpellCheckProvider::HasWordCharacters(
243    const base::string16& text,
244    int index) const {
245  const base::char16* data = text.data();
246  int length = text.length();
247  while (index < length) {
248    uint32 code = 0;
249    U16_NEXT(data, index, length, code);
250    UErrorCode error = U_ZERO_ERROR;
251    if (uscript_getScript(code, &error) != USCRIPT_COMMON)
252      return true;
253  }
254  return false;
255}
256
257#if defined(OS_MACOSX)
258void SpellCheckProvider::OnAdvanceToNextMisspelling() {
259  if (!render_view()->GetWebView())
260    return;
261  render_view()->GetWebView()->focusedFrame()->executeCommand(
262      WebString::fromUTF8("AdvanceToNextMisspelling"));
263}
264
265void SpellCheckProvider::OnRespondTextCheck(
266    int identifier,
267    const std::vector<SpellCheckResult>& results) {
268  // TODO(groby): Unify with SpellCheckProvider::OnRespondSpellingService
269  DCHECK(spellcheck_);
270  WebTextCheckingCompletion* completion =
271      text_check_completions_.Lookup(identifier);
272  if (!completion)
273    return;
274  text_check_completions_.Remove(identifier);
275  blink::WebVector<blink::WebTextCheckingResult> textcheck_results;
276  spellcheck_->CreateTextCheckingResults(SpellCheck::DO_NOT_MODIFY,
277                                         0,
278                                         base::string16(),
279                                         results,
280                                         &textcheck_results);
281  completion->didFinishCheckingText(textcheck_results);
282
283  // TODO(groby): Add request caching once OSX reports back original request.
284  // (cf. SpellCheckProvider::OnRespondSpellingService)
285  // Cache the request and the converted results.
286}
287
288void SpellCheckProvider::OnToggleSpellPanel(bool is_currently_visible) {
289  if (!render_view()->GetWebView())
290    return;
291  // We need to tell the webView whether the spelling panel is visible or not so
292  // that it won't need to make ipc calls later.
293  spelling_panel_visible_ = is_currently_visible;
294  render_view()->GetWebView()->focusedFrame()->executeCommand(
295      WebString::fromUTF8("ToggleSpellPanel"));
296}
297#endif
298
299void SpellCheckProvider::EnableSpellcheck(bool enable) {
300  if (!render_view()->GetWebView())
301    return;
302
303  WebFrame* frame = render_view()->GetWebView()->focusedFrame();
304  frame->enableContinuousSpellChecking(enable);
305  if (!enable)
306    frame->removeSpellingMarkers();
307}
308
309bool SpellCheckProvider::SatisfyRequestFromCache(
310    const base::string16& text,
311    WebTextCheckingCompletion* completion) {
312  size_t last_length = last_request_.length();
313
314  // Send back the |last_results_| if the |last_request_| is a substring of
315  // |text| and |text| does not have more words to check. Provider cannot cancel
316  // the spellcheck request here, because WebKit might have discarded the
317  // previous spellcheck results and erased the spelling markers in response to
318  // the user editing the text.
319  base::string16 request(text);
320  size_t text_length = request.length();
321  if (text_length >= last_length &&
322      !request.compare(0, last_length, last_request_)) {
323    if (text_length == last_length || !HasWordCharacters(text, last_length)) {
324      completion->didFinishCheckingText(last_results_);
325      return true;
326    }
327    int code = 0;
328    int length = static_cast<int>(text_length);
329    U16_PREV(text.data(), 0, length, code);
330    UErrorCode error = U_ZERO_ERROR;
331    if (uscript_getScript(code, &error) != USCRIPT_COMMON) {
332      completion->didCancelCheckingText();
333      return true;
334    }
335  }
336  // Create a subset of the cached results and return it if the given text is a
337  // substring of the cached text.
338  if (text_length < last_length &&
339      !last_request_.compare(0, text_length, request)) {
340    size_t result_size = 0;
341    for (size_t i = 0; i < last_results_.size(); ++i) {
342      size_t start = last_results_[i].location;
343      size_t end = start + last_results_[i].length;
344      if (start <= text_length && end <= text_length)
345        ++result_size;
346    }
347    if (result_size > 0) {
348      blink::WebVector<blink::WebTextCheckingResult> results(result_size);
349      for (size_t i = 0; i < result_size; ++i) {
350        results[i].decoration = last_results_[i].decoration;
351        results[i].location = last_results_[i].location;
352        results[i].length = last_results_[i].length;
353        results[i].replacement = last_results_[i].replacement;
354      }
355      completion->didFinishCheckingText(results);
356      return true;
357    }
358  }
359
360  return false;
361}
362