blob: d8d64664c37ef7d0514aafe33733b3eb91256879 [file] [log] [blame]
// Copyright 2018 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 "third_party/blink/renderer/core/editing/finder/find_task_controller.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/idle_request_options.h"
#include "third_party/blink/renderer/core/dom/range.h"
#include "third_party/blink/renderer/core/dom/scripted_idle_task_controller.h"
#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
#include "third_party/blink/renderer/core/editing/finder/find_options.h"
#include "third_party/blink/renderer/core/editing/finder/text_finder.h"
#include "third_party/blink/renderer/core/editing/iterators/search_buffer.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
#include "third_party/blink/renderer/platform/histogram.h"
#include "third_party/blink/renderer/platform/wtf/time.h"
namespace blink {
namespace {
const int kFindingTimeoutMS = 100;
constexpr TimeDelta kFindTaskTestTimeout = TimeDelta::FromSeconds(10);
} // namespace
class FindTaskController::IdleFindTask
: public ScriptedIdleTaskController::IdleTask {
public:
static IdleFindTask* Create(FindTaskController* controller,
Document* document,
int identifier,
const WebString& search_text,
const mojom::blink::FindOptions& options) {
return new IdleFindTask(controller, document, identifier, search_text,
options);
}
void Dispose() {
DCHECK_GT(callback_handle_, 0);
document_->CancelIdleCallback(callback_handle_);
}
void ForceInvocationForTesting() {
invoke(IdleDeadline::Create(CurrentTimeTicks() + kFindTaskTestTimeout,
IdleDeadline::CallbackType::kCalledWhenIdle));
}
void Trace(Visitor* visitor) override {
visitor->Trace(controller_);
visitor->Trace(document_);
ScriptedIdleTaskController::IdleTask::Trace(visitor);
}
private:
IdleFindTask(FindTaskController* controller,
Document* document,
int identifier,
const WebString& search_text,
const mojom::blink::FindOptions& options)
: document_(document),
controller_(controller),
identifier_(identifier),
search_text_(search_text),
options_(options.Clone()) {
DCHECK(document_);
// We need to add deadline because some webpages might have frames
// that are always busy, resulting in bad experience in find-in-page
// because the scoping tasks are not run.
// See crbug.com/893465.
IdleRequestOptions request_options;
request_options.setTimeout(kFindingTimeoutMS);
callback_handle_ = document_->RequestIdleCallback(this, request_options);
}
void invoke(IdleDeadline* deadline) override {
if (!controller_->ShouldFindMatches(search_text_, *options_)) {
controller_->DidFinishTask(identifier_, search_text_, *options_,
true /* finished_whole_request */,
PositionInFlatTree(), 0 /* match_count */);
}
const TimeDelta time_available =
TimeDelta::FromMillisecondsD(deadline->timeRemaining());
const TimeTicks start_time = CurrentTimeTicks();
PositionInFlatTree search_start = PositionInFlatTree::FirstPositionInNode(
*controller_->GetLocalFrame()->GetDocument());
PositionInFlatTree search_end = PositionInFlatTree::LastPositionInNode(
*controller_->GetLocalFrame()->GetDocument());
DCHECK_EQ(search_start.GetDocument(), search_end.GetDocument());
if (Range* resume_from_range = controller_->ResumeFindingFromRange()) {
// This is a continuation of a finding operation that timed out and didn't
// complete last time around, so we should start from where we left off.
DCHECK(resume_from_range->collapsed());
search_start = FromPositionInDOMTree<EditingInFlatTreeStrategy>(
resume_from_range->EndPosition());
if (search_start.GetDocument() != search_end.GetDocument())
return;
}
// TODO(editing-dev): Use of updateStyleAndLayoutIgnorePendingStylesheets
// needs to be audited. see http://crbug.com/590369 for more details.
search_start.GetDocument()->UpdateStyleAndLayoutIgnorePendingStylesheets();
int match_count = 0;
bool full_range_searched = false;
PositionInFlatTree next_scoping_start;
do {
// Find next occurrence of the search string.
// FIXME: (http://crbug.com/6818) This WebKit operation may run for longer
// than the timeout value, and is not interruptible as it is currently
// written. We may need to rewrite it with interruptibility in mind, or
// find an alternative.
const EphemeralRangeInFlatTree result = FindPlainText(
EphemeralRangeInFlatTree(search_start, search_end), search_text_,
options_->match_case ? 0 : kCaseInsensitive);
if (result.IsCollapsed()) {
// Not found.
full_range_searched = true;
break;
}
Range* result_range = Range::Create(
result.GetDocument(), ToPositionInDOMTree(result.StartPosition()),
ToPositionInDOMTree(result.EndPosition()));
if (result_range->collapsed()) {
// resultRange will be collapsed if the matched text spans over multiple
// TreeScopes. FIXME: Show such matches to users.
search_start = result.EndPosition();
if (deadline->timeRemaining() > 0)
continue;
break;
}
++match_count;
controller_->DidFindMatch(identifier_, result_range);
// Set the new start for the search range to be the end of the previous
// result range. There is no need to use a VisiblePosition here,
// since findPlainText will use a TextIterator to go over the visible
// text nodes.
search_start = result.EndPosition();
next_scoping_start = search_start;
} while (deadline->timeRemaining() > 0);
const TimeDelta time_spent = CurrentTimeTicks() - start_time;
UMA_HISTOGRAM_TIMES("WebCore.FindInPage.ScopingTime",
time_spent - time_available);
controller_->DidFinishTask(identifier_, search_text_, *options_,
full_range_searched, next_scoping_start,
match_count);
}
Member<Document> document_;
Member<FindTaskController> controller_;
int callback_handle_ = 0;
const int identifier_;
const WebString search_text_;
mojom::blink::FindOptionsPtr options_;
};
FindTaskController::FindTaskController(WebLocalFrameImpl& owner_frame,
TextFinder& text_finder)
: owner_frame_(owner_frame),
text_finder_(text_finder),
resume_finding_from_range_(nullptr) {}
void FindTaskController::StartRequest(
int identifier,
const WebString& search_text,
const mojom::blink::FindOptions& options) {
// This is a brand new search, so we need to reset everything.
finding_in_progress_ = true;
current_match_count_ = 0;
RequestIdleFindTask(identifier, search_text, options);
}
void FindTaskController::CancelPendingRequest() {
if (idle_find_task_) {
idle_find_task_->Dispose();
idle_find_task_.Clear();
}
if (finding_in_progress_)
last_find_request_completed_with_no_matches_ = false;
finding_in_progress_ = false;
resume_finding_from_range_ = nullptr;
}
void FindTaskController::RequestIdleFindTask(
int identifier,
const WebString& search_text,
const mojom::blink::FindOptions& options) {
DCHECK_EQ(idle_find_task_, nullptr);
idle_find_task_ = IdleFindTask::Create(this, GetLocalFrame()->GetDocument(),
identifier, search_text, options);
// If it's for testing, run the task immediately.
// TODO(rakina): Change to use general solution when it's available.
// https://crbug.com/875203
if (options.run_synchronously_for_testing)
idle_find_task_->ForceInvocationForTesting();
}
void FindTaskController::DidFinishTask(
int identifier,
const WebString& search_text,
const mojom::blink::FindOptions& options,
bool finished_whole_request,
PositionInFlatTree next_starting_position,
int match_count) {
if (idle_find_task_) {
idle_find_task_->Dispose();
idle_find_task_.Clear();
}
// Remember what we search for last time, so we can skip searching if more
// letters are added to the search string (and last outcome was 0).
last_search_string_ = search_text;
if (next_starting_position.IsNotNull()) {
resume_finding_from_range_ =
Range::Create(*next_starting_position.GetDocument(),
ToPositionInDOMTree(next_starting_position),
ToPositionInDOMTree(next_starting_position));
}
if (match_count > 0) {
text_finder_->UpdateMatches(identifier, match_count,
finished_whole_request);
}
if (!finished_whole_request) {
// Idle task ran out of time, request for another one.
RequestIdleFindTask(identifier, search_text, options);
return; // Done for now, resume work later.
}
text_finder_->FinishCurrentScopingEffort(identifier);
last_find_request_completed_with_no_matches_ = !current_match_count_;
finding_in_progress_ = false;
}
LocalFrame* FindTaskController::GetLocalFrame() const {
return OwnerFrame().GetFrame();
}
bool FindTaskController::ShouldFindMatches(
const String& search_text,
const mojom::blink::FindOptions& options) {
// Don't scope if we can't find a frame or a view.
// The user may have closed the tab/application, so abort.
LocalFrame* frame = GetLocalFrame();
if (!frame || !frame->View() || !frame->GetPage())
return false;
DCHECK(frame->GetDocument());
DCHECK(frame->View());
if (options.force)
return true;
if (!OwnerFrame().HasVisibleContent())
return false;
// If the frame completed the scoping operation and found 0 matches the last
// time it was searched, then we don't have to search it again if the user is
// just adding to the search string or sending the same search string again.
if (last_find_request_completed_with_no_matches_ &&
!last_search_string_.IsEmpty()) {
// Check to see if the search string prefixes match.
String previous_search_prefix =
search_text.Substring(0, last_search_string_.length());
if (previous_search_prefix == last_search_string_)
return false; // Don't search this frame, it will be fruitless.
}
return true;
}
void FindTaskController::DidFindMatch(int identifier, Range* result_range) {
current_match_count_++;
text_finder_->DidFindMatch(identifier, current_match_count_, result_range);
}
void FindTaskController::Trace(Visitor* visitor) {
visitor->Trace(owner_frame_);
visitor->Trace(text_finder_);
visitor->Trace(idle_find_task_);
visitor->Trace(resume_finding_from_range_);
}
} // namespace blink