Commit 3a98eaed authored by rlanday's avatar rlanday Committed by Commit Bot

Add SpellChecker::GetSpellCheckMarkerTouchingSelection()

This CL makes some changes to SpellChecker that I need to implement the Android
spellcheck menu in Chrome. On most platforms, users can apply spellcheck
suggestions by right-clicking in the interior of a misspelled word (nothing
happens if you right-click at an endpoint) and selecting a replacement. On
Android, the spellcheck menu is triggered by tapping on a word (either in the
interior or at an endpoint). This means we need to do two things (both done in
this CL):

- Expose a way for a caller to determine if the selection (which may be an
  empty caret) is currently touching a spellcheck marker. If some text is
  actually selected, we can just see if any spellcheck markers on the text
  nodes making up the selected text intersect the selection. If we just have a
  caret selection, we need to create a range containing one character on either
  side and do the same operation. This method is being added as
  SpellChecker::GetSpellCheckMarkerTouchingSelection(). (Note: this will
  eventually be useful for suggestion markers as well once those are added, so
  we might want to consider putting this method somewhere else).

- Make SpellChecker::ReplaceMisspelledRange() (which looks for a spellcheck
  marker intersecting the selection) work in the case where we have a caret
  selection touching the spellcheck marker. This is done by using the new
  GetSpellCheckMarkerTouchingSelection() method to find the marker.

BUG=715365

Review-Url: https://codereview.chromium.org/2925363002
Cr-Commit-Position: refs/heads/master@{#478380}
parent cd4a0934
...@@ -43,6 +43,7 @@ ...@@ -43,6 +43,7 @@
#include "core/editing/commands/TypingCommand.h" #include "core/editing/commands/TypingCommand.h"
#include "core/editing/iterators/CharacterIterator.h" #include "core/editing/iterators/CharacterIterator.h"
#include "core/editing/markers/DocumentMarkerController.h" #include "core/editing/markers/DocumentMarkerController.h"
#include "core/editing/markers/SpellCheckMarker.h"
#include "core/editing/spellcheck/IdleSpellCheckCallback.h" #include "core/editing/spellcheck/IdleSpellCheckCallback.h"
#include "core/editing/spellcheck/SpellCheckRequester.h" #include "core/editing/spellcheck/SpellCheckRequester.h"
#include "core/editing/spellcheck/SpellCheckerClient.h" #include "core/editing/spellcheck/SpellCheckerClient.h"
...@@ -91,6 +92,45 @@ SelectionInDOMTree SelectWord(const VisiblePosition& position) { ...@@ -91,6 +92,45 @@ SelectionInDOMTree SelectWord(const VisiblePosition& position) {
.Build(); .Build();
} }
EphemeralRange ExpandSelectionRangeIfNecessary(
const VisibleSelection& selection) {
DCHECK(!selection.IsNone());
// If some text is actually selected, we can use the selection range as-is to
// check for a marker. If no text is selected (we just have a caret
// somewhere), we need to expand one character on either side so we can find
// a spelling marker immediately before or after the caret.
// (The spelling markers on these words may be anchored to a different node
// than the collapsed selection's Position is, but once we expand the
// selection, if we're next to a marker, either the start or end point should
// now be anchored relative to the same text node as that marker.)
// Some text is actually selected
if (selection.IsRange())
return EphemeralRange(selection.Start(), selection.End());
// No text is actually selected, need to expand the selection range. This is
// to make sure we can find a marker touching the caret. E.g. if we have:
// <span>word1 <b>word2</b></span>
// with a caret selection immediately before "word2", there's one text node
// immediately before the caret ("word1 ") and one immediately after
// ("word2"). Expanding the selection by one character on either side ensures
// we get a range that intersects both neighboring text nodes (if they exist).
const VisiblePosition& caret_position = selection.VisibleStart();
const Position& previous_position =
PreviousPositionOf(caret_position).DeepEquivalent();
const Position& next_position =
NextPositionOf(caret_position).DeepEquivalent();
return EphemeralRange(
previous_position.IsNull() ? caret_position.DeepEquivalent()
: previous_position,
next_position.IsNull() ? caret_position.DeepEquivalent() : next_position);
}
} // namespace } // namespace
SpellChecker* SpellChecker::Create(LocalFrame& frame) { SpellChecker* SpellChecker::Create(LocalFrame& frame) {
...@@ -811,51 +851,57 @@ void SpellChecker::RemoveSpellingAndGrammarMarkers(const HTMLElement& element, ...@@ -811,51 +851,57 @@ void SpellChecker::RemoveSpellingAndGrammarMarkers(const HTMLElement& element,
} }
} }
void SpellChecker::ReplaceMisspelledRange(const String& text) { Optional<std::pair<Node*, SpellCheckMarker*>>
EphemeralRange caret_range = GetFrame() SpellChecker::GetSpellCheckMarkerTouchingSelection() {
.Selection() const VisibleSelection& selection =
.ComputeVisibleSelectionInDOMTree() GetFrame().Selection().ComputeVisibleSelectionInDOMTree();
.ToNormalizedEphemeralRange(); if (selection.IsNone())
if (caret_range.IsNull()) return Optional<std::pair<Node*, SpellCheckMarker*>>();
return;
const EphemeralRange& range_to_check =
ExpandSelectionRangeIfNecessary(selection);
Node* const start_container =
range_to_check.StartPosition().ComputeContainerNode();
const unsigned start_offset =
range_to_check.StartPosition().ComputeOffsetInContainerNode();
Node* const end_container =
range_to_check.EndPosition().ComputeContainerNode();
const unsigned end_offset =
range_to_check.EndPosition().ComputeOffsetInContainerNode();
for (Node& node : range_to_check.Nodes()) {
const DocumentMarkerVector& markers_in_node =
GetFrame().GetDocument()->Markers().MarkersFor(
&node, DocumentMarker::MisspellingMarkers());
for (DocumentMarker* marker : markers_in_node) {
if (node == start_container && marker->EndOffset() <= start_offset)
continue;
if (node == end_container && marker->StartOffset() >= end_offset)
continue;
Node* const caret_start_container = return std::make_pair(&node, &ToSpellCheckMarker(*marker));
caret_range.StartPosition().ComputeContainerNode(); }
Node* const caret_end_container = }
caret_range.EndPosition().ComputeContainerNode();
// We don't currently support the case where a misspelling spans multiple // No marker found
// nodes return Optional<std::pair<Node*, SpellCheckMarker*>>();
if (caret_start_container != caret_end_container) }
return;
const unsigned caret_start_offset = void SpellChecker::ReplaceMisspelledRange(const String& text) {
caret_range.StartPosition().ComputeOffsetInContainerNode(); const Optional<std::pair<Node*, SpellCheckMarker*>>& node_and_marker =
const unsigned caret_end_offset = GetSpellCheckMarkerTouchingSelection();
caret_range.EndPosition().ComputeOffsetInContainerNode(); if (!node_and_marker)
const DocumentMarkerVector& markers_in_node =
GetFrame().GetDocument()->Markers().MarkersFor(
caret_start_container, DocumentMarker::MisspellingMarkers());
const auto marker_it =
std::find_if(markers_in_node.begin(), markers_in_node.end(),
[=](const DocumentMarker* marker) {
return marker->StartOffset() < caret_end_offset &&
marker->EndOffset() > caret_start_offset;
});
if (marker_it == markers_in_node.end())
return; return;
const DocumentMarker* found_marker = *marker_it; Node* const container_node = node_and_marker.value().first;
EphemeralRange marker_range = EphemeralRange( const SpellCheckMarker* const marker = node_and_marker.value().second;
Position(caret_start_container, found_marker->StartOffset()),
Position(caret_start_container, found_marker->EndOffset()));
if (marker_range.IsNull())
return;
GetFrame().Selection().SetSelection( GetFrame().Selection().SetSelection(
SelectionInDOMTree::Builder().SetBaseAndExtent(marker_range).Build()); SelectionInDOMTree::Builder()
.Collapse(Position(container_node, marker->StartOffset()))
.Extend(Position(container_node, marker->EndOffset()))
.Build());
Document& current_document = *GetFrame().GetDocument(); Document& current_document = *GetFrame().GetDocument();
......
...@@ -40,6 +40,7 @@ class IdleSpellCheckCallback; ...@@ -40,6 +40,7 @@ class IdleSpellCheckCallback;
class LocalFrame; class LocalFrame;
class ReplaceSelectionCommand; class ReplaceSelectionCommand;
class SpellCheckerClient; class SpellCheckerClient;
class SpellCheckMarker;
class SpellCheckRequest; class SpellCheckRequest;
class SpellCheckRequester; class SpellCheckRequester;
class TextCheckerClient; class TextCheckerClient;
...@@ -72,6 +73,8 @@ class CORE_EXPORT SpellChecker final : public GarbageCollected<SpellChecker> { ...@@ -72,6 +73,8 @@ class CORE_EXPORT SpellChecker final : public GarbageCollected<SpellChecker> {
void RespondToChangedContents(); void RespondToChangedContents();
void RespondToChangedSelection(const Position& old_selection_start, void RespondToChangedSelection(const Position& old_selection_start,
FrameSelection::SetSelectionOptions); FrameSelection::SetSelectionOptions);
Optional<std::pair<Node*, SpellCheckMarker*>>
GetSpellCheckMarkerTouchingSelection();
void ReplaceMisspelledRange(const String&); void ReplaceMisspelledRange(const String&);
void RemoveSpellingMarkers(); void RemoveSpellingMarkers();
void RemoveSpellingMarkersUnderWords(const Vector<String>& words); void RemoveSpellingMarkersUnderWords(const Vector<String>& words);
......
...@@ -129,4 +129,254 @@ TEST_F(SpellCheckerTest, MarkAndReplaceForHandlesMultipleReplacements) { ...@@ -129,4 +129,254 @@ TEST_F(SpellCheckerTest, MarkAndReplaceForHandlesMultipleReplacements) {
ToSpellCheckMarker(GetDocument().Markers().Markers()[0])->Description()); ToSpellCheckMarker(GetDocument().Markers().Markers()[0])->Description());
} }
TEST_F(SpellCheckerTest,
GetSpellCheckMarkerTouchingSelection_FirstCharSelected) {
SetBodyContent(
"<div contenteditable>"
"spllchck"
"</div>");
Element* div = GetDocument().QuerySelector("div");
Node* text = div->firstChild();
GetDocument().Markers().AddSpellingMarker(
EphemeralRange(Position(text, 0), Position(text, 8)));
GetDocument().GetFrame()->Selection().SetSelection(
SelectionInDOMTree::Builder()
.SetBaseAndExtent(Position(text, 0), Position(text, 1))
.Build());
Optional<std::pair<Node*, SpellCheckMarker*>> result =
GetDocument()
.GetFrame()
->GetSpellChecker()
.GetSpellCheckMarkerTouchingSelection();
EXPECT_TRUE(result);
}
TEST_F(SpellCheckerTest,
GetSpellCheckMarkerTouchingSelection_LastCharSelected) {
SetBodyContent(
"<div contenteditable>"
"spllchck"
"</div>");
Element* div = GetDocument().QuerySelector("div");
Node* text = div->firstChild();
GetDocument().Markers().AddSpellingMarker(
EphemeralRange(Position(text, 0), Position(text, 8)));
GetDocument().GetFrame()->Selection().SetSelection(
SelectionInDOMTree::Builder()
.SetBaseAndExtent(Position(text, 7), Position(text, 8))
.Build());
Optional<std::pair<Node*, SpellCheckMarker*>> result =
GetDocument()
.GetFrame()
->GetSpellChecker()
.GetSpellCheckMarkerTouchingSelection();
EXPECT_TRUE(result);
}
TEST_F(SpellCheckerTest,
GetSpellCheckMarkerTouchingSelection_SingleCharWordSelected) {
SetBodyContent(
"<div contenteditable>"
"s"
"</div>");
Element* div = GetDocument().QuerySelector("div");
Node* text = div->firstChild();
GetDocument().Markers().AddSpellingMarker(
EphemeralRange(Position(text, 0), Position(text, 1)));
GetDocument().GetFrame()->Selection().SetSelection(
SelectionInDOMTree::Builder()
.SetBaseAndExtent(Position(text, 0), Position(text, 1))
.Build());
Optional<std::pair<Node*, SpellCheckMarker*>> result =
GetDocument()
.GetFrame()
->GetSpellChecker()
.GetSpellCheckMarkerTouchingSelection();
EXPECT_TRUE(result);
}
TEST_F(SpellCheckerTest,
GetSpellCheckMarkerTouchingSelection_CaretLeftOfSingleCharWord) {
SetBodyContent(
"<div contenteditable>"
"s"
"</div>");
Element* div = GetDocument().QuerySelector("div");
Node* text = div->firstChild();
GetDocument().Markers().AddSpellingMarker(
EphemeralRange(Position(text, 0), Position(text, 1)));
GetDocument().GetFrame()->Selection().SetSelection(
SelectionInDOMTree::Builder()
.SetBaseAndExtent(Position(text, 0), Position(text, 0))
.Build());
Optional<std::pair<Node*, SpellCheckMarker*>> result =
GetDocument()
.GetFrame()
->GetSpellChecker()
.GetSpellCheckMarkerTouchingSelection();
EXPECT_TRUE(result);
}
TEST_F(SpellCheckerTest,
GetSpellCheckMarkerTouchingSelection_CaretRightOfSingleCharWord) {
SetBodyContent(
"<div contenteditable>"
"s"
"</div>");
Element* div = GetDocument().QuerySelector("div");
Node* text = div->firstChild();
GetDocument().Markers().AddSpellingMarker(
EphemeralRange(Position(text, 0), Position(text, 1)));
GetDocument().GetFrame()->Selection().SetSelection(
SelectionInDOMTree::Builder()
.SetBaseAndExtent(Position(text, 1), Position(text, 1))
.Build());
Optional<std::pair<Node*, SpellCheckMarker*>> result =
GetDocument()
.GetFrame()
->GetSpellChecker()
.GetSpellCheckMarkerTouchingSelection();
EXPECT_TRUE(result);
}
TEST_F(SpellCheckerTest,
GetSpellCheckMarkerTouchingSelection_CaretLeftOfMultiCharWord) {
SetBodyContent(
"<div contenteditable>"
"spllchck"
"</div>");
Element* div = GetDocument().QuerySelector("div");
Node* text = div->firstChild();
GetDocument().Markers().AddSpellingMarker(
EphemeralRange(Position(text, 0), Position(text, 8)));
GetDocument().GetFrame()->Selection().SetSelection(
SelectionInDOMTree::Builder()
.SetBaseAndExtent(Position(text, 0), Position(text, 0))
.Build());
Optional<std::pair<Node*, SpellCheckMarker*>> result =
GetDocument()
.GetFrame()
->GetSpellChecker()
.GetSpellCheckMarkerTouchingSelection();
EXPECT_TRUE(result);
}
TEST_F(SpellCheckerTest,
GetSpellCheckMarkerTouchingSelection_CaretRightOfMultiCharWord) {
SetBodyContent(
"<div contenteditable>"
"spllchck"
"</div>");
Element* div = GetDocument().QuerySelector("div");
Node* text = div->firstChild();
GetDocument().Markers().AddSpellingMarker(
EphemeralRange(Position(text, 0), Position(text, 8)));
GetDocument().GetFrame()->Selection().SetSelection(
SelectionInDOMTree::Builder()
.SetBaseAndExtent(Position(text, 8), Position(text, 8))
.Build());
Optional<std::pair<Node*, SpellCheckMarker*>> result =
GetDocument()
.GetFrame()
->GetSpellChecker()
.GetSpellCheckMarkerTouchingSelection();
EXPECT_TRUE(result);
}
TEST_F(SpellCheckerTest,
GetSpellCheckMarkerTouchingSelection_CaretMiddleOfWord) {
SetBodyContent(
"<div contenteditable>"
"spllchck"
"</div>");
Element* div = GetDocument().QuerySelector("div");
Node* text = div->firstChild();
GetDocument().Markers().AddSpellingMarker(
EphemeralRange(Position(text, 0), Position(text, 8)));
GetDocument().GetFrame()->Selection().SetSelection(
SelectionInDOMTree::Builder()
.SetBaseAndExtent(Position(text, 4), Position(text, 4))
.Build());
Optional<std::pair<Node*, SpellCheckMarker*>> result =
GetDocument()
.GetFrame()
->GetSpellChecker()
.GetSpellCheckMarkerTouchingSelection();
EXPECT_TRUE(result);
}
TEST_F(SpellCheckerTest,
GetSpellCheckMarkerTouchingSelection_CaretOneCharLeftOfMisspelling) {
SetBodyContent(
"<div contenteditable>"
"a spllchck"
"</div>");
Element* div = GetDocument().QuerySelector("div");
Node* text = div->firstChild();
GetDocument().Markers().AddSpellingMarker(
EphemeralRange(Position(text, 2), Position(text, 10)));
GetDocument().GetFrame()->Selection().SetSelection(
SelectionInDOMTree::Builder()
.SetBaseAndExtent(Position(text, 1), Position(text, 1))
.Build());
Optional<std::pair<Node*, SpellCheckMarker*>> result =
GetDocument()
.GetFrame()
->GetSpellChecker()
.GetSpellCheckMarkerTouchingSelection();
EXPECT_FALSE(result);
}
TEST_F(SpellCheckerTest,
GetSpellCheckMarkerTouchingSelection_CaretOneCharRightOfMisspelling) {
SetBodyContent(
"<div contenteditable>"
"spllchck a"
"</div>");
Element* div = GetDocument().QuerySelector("div");
Node* text = div->firstChild();
GetDocument().Markers().AddSpellingMarker(
EphemeralRange(Position(text, 0), Position(text, 8)));
GetDocument().GetFrame()->Selection().SetSelection(
SelectionInDOMTree::Builder()
.SetBaseAndExtent(Position(text, 9), Position(text, 9))
.Build());
Optional<std::pair<Node*, SpellCheckMarker*>> result =
GetDocument()
.GetFrame()
->GetSpellChecker()
.GetSpellCheckMarkerTouchingSelection();
EXPECT_FALSE(result);
}
} // namespace blink } // namespace blink
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment