| // 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_buffer.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/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 MakeGarbageCollected<IdleFindTask>(controller, document, identifier, |
| search_text, options); |
| } |
| |
| 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 = IdleRequestOptions::Create(); |
| request_options->setTimeout(kFindingTimeoutMS); |
| callback_handle_ = document_->RequestIdleCallback(this, request_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: |
| 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(); |
| |
| Document& document = *controller_->GetLocalFrame()->GetDocument(); |
| PositionInFlatTree search_start = |
| PositionInFlatTree::FirstPositionInNode(document); |
| PositionInFlatTree search_end; |
| if (document.documentElement() && document.documentElement()->lastChild()) { |
| search_end = PositionInFlatTree::AfterNode( |
| *document.documentElement()->lastChild()); |
| } else { |
| search_end = PositionInFlatTree::LastPositionInNode(document); |
| } |
| 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_task_start_position; |
| |
| blink::FindOptions find_options = |
| (options_->forward ? 0 : kBackwards) | |
| (options_->match_case ? 0 : kCaseInsensitive) | |
| (options_->find_next ? 0 : kStartInSelection); |
| |
| do { |
| // Find in the whole block. |
| FindBuffer buffer(EphemeralRangeInFlatTree(search_start, search_end)); |
| std::unique_ptr<FindBuffer::Results> match_results = |
| buffer.FindMatches(search_text_, find_options); |
| for (FindBuffer::BufferMatchResult match : *match_results) { |
| const EphemeralRangeInFlatTree ephemeral_match_range = |
| buffer.RangeFromBufferIndex(match.start, |
| match.start + match.length); |
| Range* const match_range = Range::Create( |
| ephemeral_match_range.GetDocument(), |
| ToPositionInDOMTree(ephemeral_match_range.StartPosition()), |
| ToPositionInDOMTree(ephemeral_match_range.EndPosition())); |
| if (match_range->collapsed()) { |
| // resultRange will be collapsed if the matched text spans over |
| // multiple TreeScopes. TODO(rakina): Show such matches to users. |
| next_task_start_position = ephemeral_match_range.EndPosition(); |
| continue; |
| } |
| ++match_count; |
| controller_->DidFindMatch(identifier_, match_range); |
| } |
| // At this point, all text in the block collected above has been |
| // processed. Now we move to the next block if there's any, |
| // otherwise we should stop. |
| search_start = buffer.PositionAfterBlock(); |
| if (search_start.IsNull()) { |
| full_range_searched = true; |
| break; |
| } |
| next_task_start_position = 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_task_start_position, |
| 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 |