| // Copyright 2016 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/spellcheck/idle_spell_check_controller.h" |
| |
| #include "third_party/blink/public/platform/task_type.h" |
| #include "third_party/blink/renderer/core/dom/idle_request_options.h" |
| #include "third_party/blink/renderer/core/editing/commands/undo_stack.h" |
| #include "third_party/blink/renderer/core/editing/commands/undo_step.h" |
| #include "third_party/blink/renderer/core/editing/editing_utilities.h" |
| #include "third_party/blink/renderer/core/editing/editor.h" |
| #include "third_party/blink/renderer/core/editing/ephemeral_range.h" |
| #include "third_party/blink/renderer/core/editing/frame_selection.h" |
| #include "third_party/blink/renderer/core/editing/iterators/character_iterator.h" |
| #include "third_party/blink/renderer/core/editing/selection_template.h" |
| #include "third_party/blink/renderer/core/editing/spellcheck/cold_mode_spell_check_requester.h" |
| #include "third_party/blink/renderer/core/editing/spellcheck/hot_mode_spell_check_requester.h" |
| #include "third_party/blink/renderer/core/editing/spellcheck/spell_check_requester.h" |
| #include "third_party/blink/renderer/core/editing/spellcheck/spell_checker.h" |
| #include "third_party/blink/renderer/core/editing/visible_selection.h" |
| #include "third_party/blink/renderer/core/editing/visible_units.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h" |
| #include "third_party/blink/renderer/platform/runtime_enabled_features.h" |
| #include "third_party/blink/renderer/platform/wtf/time.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| constexpr TimeDelta kColdModeTimerInterval = TimeDelta::FromMilliseconds(1000); |
| constexpr TimeDelta kConsecutiveColdModeTimerInterval = |
| TimeDelta::FromMilliseconds(200); |
| const int kHotModeRequestTimeoutMS = 200; |
| const int kInvalidHandle = -1; |
| const int kDummyHandleForForcedInvocation = -2; |
| constexpr TimeDelta kIdleSpellcheckTestTimeout = TimeDelta::FromSeconds(10); |
| |
| } // namespace |
| |
| class IdleSpellCheckController::IdleCallback final |
| : public ScriptedIdleTaskController::IdleTask { |
| public: |
| static IdleCallback* Create(IdleSpellCheckController* controller) { |
| return new IdleCallback(controller); |
| } |
| |
| void Trace(blink::Visitor* visitor) final { |
| visitor->Trace(controller_); |
| ScriptedIdleTaskController::IdleTask::Trace(visitor); |
| } |
| |
| private: |
| explicit IdleCallback(IdleSpellCheckController* controller) |
| : controller_(controller) {} |
| |
| void invoke(IdleDeadline* deadline) final { controller_->Invoke(deadline); } |
| |
| const Member<IdleSpellCheckController> controller_; |
| |
| DISALLOW_COPY_AND_ASSIGN(IdleCallback); |
| }; |
| |
| IdleSpellCheckController::~IdleSpellCheckController() = default; |
| |
| void IdleSpellCheckController::Trace(blink::Visitor* visitor) { |
| visitor->Trace(frame_); |
| visitor->Trace(cold_mode_requester_); |
| DocumentShutdownObserver::Trace(visitor); |
| } |
| |
| IdleSpellCheckController* IdleSpellCheckController::Create(LocalFrame& frame) { |
| return new IdleSpellCheckController(frame); |
| } |
| |
| IdleSpellCheckController::IdleSpellCheckController(LocalFrame& frame) |
| : state_(State::kInactive), |
| idle_callback_handle_(kInvalidHandle), |
| frame_(frame), |
| last_processed_undo_step_sequence_(0), |
| cold_mode_requester_(ColdModeSpellCheckRequester::Create(frame)), |
| cold_mode_timer_(frame.GetTaskRunner(TaskType::kInternalDefault), |
| this, |
| &IdleSpellCheckController::ColdModeTimerFired) {} |
| |
| SpellCheckRequester& IdleSpellCheckController::GetSpellCheckRequester() const { |
| return GetFrame().GetSpellChecker().GetSpellCheckRequester(); |
| } |
| |
| bool IdleSpellCheckController::IsSpellCheckingEnabled() const { |
| return GetFrame().GetSpellChecker().IsSpellCheckingEnabled(); |
| } |
| |
| void IdleSpellCheckController::DisposeIdleCallback() { |
| if (idle_callback_handle_ != kInvalidHandle && IsAvailable()) |
| GetDocument().CancelIdleCallback(idle_callback_handle_); |
| idle_callback_handle_ = kInvalidHandle; |
| } |
| |
| void IdleSpellCheckController::Deactivate() { |
| state_ = State::kInactive; |
| if (cold_mode_timer_.IsActive()) |
| cold_mode_timer_.Stop(); |
| cold_mode_requester_->ClearProgress(); |
| DisposeIdleCallback(); |
| } |
| |
| void IdleSpellCheckController::SetNeedsInvocation() { |
| if (!IsSpellCheckingEnabled() || !IsAvailable()) { |
| Deactivate(); |
| return; |
| } |
| |
| if (state_ == State::kHotModeRequested) |
| return; |
| |
| cold_mode_requester_->ClearProgress(); |
| |
| if (state_ == State::kColdModeTimerStarted) { |
| DCHECK(cold_mode_timer_.IsActive()); |
| cold_mode_timer_.Stop(); |
| } |
| |
| if (state_ == State::kColdModeRequested) |
| DisposeIdleCallback(); |
| |
| IdleRequestOptions options; |
| options.setTimeout(kHotModeRequestTimeoutMS); |
| idle_callback_handle_ = |
| GetDocument().RequestIdleCallback(IdleCallback::Create(this), options); |
| state_ = State::kHotModeRequested; |
| } |
| |
| void IdleSpellCheckController::SetNeedsColdModeInvocation() { |
| if (!RuntimeEnabledFeatures::IdleTimeColdModeSpellCheckingEnabled() || |
| !IsSpellCheckingEnabled()) { |
| Deactivate(); |
| return; |
| } |
| |
| if (state_ != State::kInactive && state_ != State::kInHotModeInvocation && |
| state_ != State::kInColdModeInvocation) |
| return; |
| |
| DCHECK(!cold_mode_timer_.IsActive()); |
| TimeDelta interval = state_ == State::kInColdModeInvocation |
| ? kConsecutiveColdModeTimerInterval |
| : kColdModeTimerInterval; |
| cold_mode_timer_.StartOneShot(interval, FROM_HERE); |
| state_ = State::kColdModeTimerStarted; |
| } |
| |
| void IdleSpellCheckController::ColdModeTimerFired(TimerBase*) { |
| DCHECK(RuntimeEnabledFeatures::IdleTimeColdModeSpellCheckingEnabled()); |
| DCHECK_EQ(State::kColdModeTimerStarted, state_); |
| |
| if (!IsSpellCheckingEnabled() || !IsAvailable()) { |
| Deactivate(); |
| return; |
| } |
| |
| idle_callback_handle_ = GetDocument().RequestIdleCallback( |
| IdleCallback::Create(this), IdleRequestOptions()); |
| state_ = State::kColdModeRequested; |
| } |
| |
| void IdleSpellCheckController::HotModeInvocation(IdleDeadline* deadline) { |
| TRACE_EVENT0("blink", "IdleSpellCheckController::hotModeInvocation"); |
| |
| // TODO(xiaochengh): Figure out if this has any performance impact. |
| GetDocument().UpdateStyleAndLayout(); |
| |
| HotModeSpellCheckRequester requester(GetSpellCheckRequester()); |
| |
| requester.CheckSpellingAt( |
| GetFrame().Selection().GetSelectionInDOMTree().Extent()); |
| |
| const uint64_t watermark = last_processed_undo_step_sequence_; |
| for (const UndoStep* step : |
| GetFrame().GetEditor().GetUndoStack().UndoSteps()) { |
| if (step->SequenceNumber() <= watermark) |
| break; |
| last_processed_undo_step_sequence_ = |
| std::max(step->SequenceNumber(), last_processed_undo_step_sequence_); |
| if (deadline->timeRemaining() == 0) |
| break; |
| // The ending selection stored in undo stack can be invalid, disconnected |
| // or have been moved to another document, so we should check its validity |
| // before using it. |
| if (!step->EndingSelection().IsValidFor(GetDocument())) |
| continue; |
| requester.CheckSpellingAt(step->EndingSelection().Extent()); |
| } |
| } |
| |
| void IdleSpellCheckController::Invoke(IdleDeadline* deadline) { |
| DCHECK_NE(idle_callback_handle_, kInvalidHandle); |
| idle_callback_handle_ = kInvalidHandle; |
| |
| if (!IsSpellCheckingEnabled() || !IsAvailable()) { |
| Deactivate(); |
| return; |
| } |
| |
| if (state_ == State::kHotModeRequested) { |
| state_ = State::kInHotModeInvocation; |
| HotModeInvocation(deadline); |
| SetNeedsColdModeInvocation(); |
| } else if (state_ == State::kColdModeRequested) { |
| DCHECK(RuntimeEnabledFeatures::IdleTimeColdModeSpellCheckingEnabled()); |
| state_ = State::kInColdModeInvocation; |
| cold_mode_requester_->Invoke(deadline); |
| if (cold_mode_requester_->FullyChecked()) |
| state_ = State::kInactive; |
| else |
| SetNeedsColdModeInvocation(); |
| } else { |
| NOTREACHED(); |
| } |
| } |
| |
| void IdleSpellCheckController::DocumentAttached(Document* document) { |
| SetContext(document); |
| } |
| |
| void IdleSpellCheckController::ContextDestroyed(Document*) { |
| Deactivate(); |
| } |
| |
| void IdleSpellCheckController::ForceInvocationForTesting() { |
| if (!IsSpellCheckingEnabled()) |
| return; |
| |
| IdleDeadline* deadline = |
| IdleDeadline::Create(CurrentTimeTicks() + kIdleSpellcheckTestTimeout, |
| IdleDeadline::CallbackType::kCalledWhenIdle); |
| |
| switch (state_) { |
| case State::kColdModeTimerStarted: |
| cold_mode_timer_.Stop(); |
| state_ = State::kColdModeRequested; |
| idle_callback_handle_ = kDummyHandleForForcedInvocation; |
| Invoke(deadline); |
| break; |
| case State::kHotModeRequested: |
| case State::kColdModeRequested: |
| GetDocument().CancelIdleCallback(idle_callback_handle_); |
| Invoke(deadline); |
| break; |
| case State::kInactive: |
| case State::kInHotModeInvocation: |
| case State::kInColdModeInvocation: |
| NOTREACHED(); |
| } |
| } |
| |
| void IdleSpellCheckController::SkipColdModeTimerForTesting() { |
| DCHECK(cold_mode_timer_.IsActive()); |
| cold_mode_timer_.Stop(); |
| ColdModeTimerFired(&cold_mode_timer_); |
| } |
| |
| void IdleSpellCheckController::SetNeedsMoreColdModeInvocationForTesting() { |
| cold_mode_requester_->SetNeedsMoreInvocationForTesting(); |
| } |
| |
| } // namespace blink |