| // 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/bindings/core/v8/v8_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_dom_window.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/scheduler/public/post_cancellable_task.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| constexpr base::TimeDelta kColdModeTimerInterval = |
| base::TimeDelta::FromMilliseconds(1000); |
| constexpr base::TimeDelta kConsecutiveColdModeTimerInterval = |
| base::TimeDelta::FromMilliseconds(200); |
| const int kHotModeRequestTimeoutMS = 200; |
| const int kInvalidHandle = -1; |
| const int kDummyHandleForForcedInvocation = -2; |
| constexpr base::TimeDelta kIdleSpellcheckTestTimeout = |
| base::TimeDelta::FromSeconds(10); |
| |
| } // namespace |
| |
| class IdleSpellCheckController::IdleCallback final : public IdleTask { |
| public: |
| explicit IdleCallback(IdleSpellCheckController* controller) |
| : controller_(controller) {} |
| |
| void Trace(Visitor* visitor) const final { |
| visitor->Trace(controller_); |
| IdleTask::Trace(visitor); |
| } |
| |
| private: |
| void invoke(IdleDeadline* deadline) final { controller_->Invoke(deadline); } |
| |
| const Member<IdleSpellCheckController> controller_; |
| |
| DISALLOW_COPY_AND_ASSIGN(IdleCallback); |
| }; |
| |
| IdleSpellCheckController::~IdleSpellCheckController() = default; |
| |
| void IdleSpellCheckController::Trace(Visitor* visitor) const { |
| visitor->Trace(cold_mode_requester_); |
| visitor->Trace(spell_check_requeseter_); |
| ExecutionContextLifecycleObserver::Trace(visitor); |
| } |
| |
| IdleSpellCheckController::IdleSpellCheckController( |
| LocalDOMWindow& window, |
| SpellCheckRequester& requester) |
| : ExecutionContextLifecycleObserver(&window), |
| state_(State::kInactive), |
| idle_callback_handle_(kInvalidHandle), |
| last_processed_undo_step_sequence_(0), |
| cold_mode_requester_( |
| MakeGarbageCollected<ColdModeSpellCheckRequester>(window)), |
| spell_check_requeseter_(requester) {} |
| |
| LocalDOMWindow& IdleSpellCheckController::GetWindow() const { |
| DCHECK(GetExecutionContext()); |
| return *To<LocalDOMWindow>(GetExecutionContext()); |
| } |
| |
| Document& IdleSpellCheckController::GetDocument() const { |
| DCHECK(GetExecutionContext()); |
| return *GetWindow().document(); |
| } |
| |
| bool IdleSpellCheckController::IsSpellCheckingEnabled() const { |
| if (!GetExecutionContext()) |
| return false; |
| return GetWindow().GetSpellChecker().IsSpellCheckingEnabled(); |
| } |
| |
| void IdleSpellCheckController::DisposeIdleCallback() { |
| if (idle_callback_handle_ != kInvalidHandle && GetExecutionContext()) |
| GetDocument().CancelIdleCallback(idle_callback_handle_); |
| idle_callback_handle_ = kInvalidHandle; |
| } |
| |
| void IdleSpellCheckController::Deactivate() { |
| state_ = State::kInactive; |
| if (cold_mode_timer_.IsActive()) |
| cold_mode_timer_.Cancel(); |
| cold_mode_requester_->ClearProgress(); |
| DisposeIdleCallback(); |
| spell_check_requeseter_->Deactivate(); |
| } |
| |
| void IdleSpellCheckController::SetNeedsInvocation() { |
| if (!IsSpellCheckingEnabled()) { |
| Deactivate(); |
| return; |
| } |
| |
| if (state_ == State::kHotModeRequested) |
| return; |
| |
| cold_mode_requester_->ClearProgress(); |
| |
| if (state_ == State::kColdModeTimerStarted) { |
| DCHECK(cold_mode_timer_.IsActive()); |
| cold_mode_timer_.Cancel(); |
| } |
| |
| if (state_ == State::kColdModeRequested) |
| DisposeIdleCallback(); |
| |
| IdleRequestOptions* options = IdleRequestOptions::Create(); |
| options->setTimeout(kHotModeRequestTimeoutMS); |
| idle_callback_handle_ = GetDocument().RequestIdleCallback( |
| MakeGarbageCollected<IdleCallback>(this), options); |
| state_ = State::kHotModeRequested; |
| } |
| |
| void IdleSpellCheckController::SetNeedsColdModeInvocation() { |
| DCHECK(IsSpellCheckingEnabled()); |
| if (state_ != State::kInactive && state_ != State::kInHotModeInvocation && |
| state_ != State::kInColdModeInvocation) |
| return; |
| |
| DCHECK(!cold_mode_timer_.IsActive()); |
| base::TimeDelta interval = state_ == State::kInColdModeInvocation |
| ? kConsecutiveColdModeTimerInterval |
| : kColdModeTimerInterval; |
| cold_mode_timer_ = PostDelayedCancellableTask( |
| *GetWindow().GetTaskRunner(TaskType::kInternalDefault), FROM_HERE, |
| WTF::Bind(&IdleSpellCheckController::ColdModeTimerFired, |
| WrapPersistent(this)), |
| interval); |
| state_ = State::kColdModeTimerStarted; |
| } |
| |
| void IdleSpellCheckController::ColdModeTimerFired() { |
| DCHECK_EQ(State::kColdModeTimerStarted, state_); |
| |
| if (!IsSpellCheckingEnabled()) { |
| Deactivate(); |
| return; |
| } |
| |
| idle_callback_handle_ = GetDocument().RequestIdleCallback( |
| MakeGarbageCollected<IdleCallback>(this), IdleRequestOptions::Create()); |
| 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(DocumentUpdateReason::kEditing); |
| |
| HotModeSpellCheckRequester requester(*spell_check_requeseter_); |
| |
| requester.CheckSpellingAt( |
| GetWindow().GetFrame()->Selection().GetSelectionInDOMTree().Extent()); |
| |
| const uint64_t watermark = last_processed_undo_step_sequence_; |
| for (const UndoStep* step : |
| GetWindow().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()) { |
| Deactivate(); |
| return; |
| } |
| |
| if (state_ == State::kHotModeRequested) { |
| state_ = State::kInHotModeInvocation; |
| HotModeInvocation(deadline); |
| SetNeedsColdModeInvocation(); |
| } else if (state_ == State::kColdModeRequested) { |
| state_ = State::kInColdModeInvocation; |
| cold_mode_requester_->Invoke(deadline); |
| if (cold_mode_requester_->FullyChecked()) |
| state_ = State::kInactive; |
| else |
| SetNeedsColdModeInvocation(); |
| } else { |
| NOTREACHED(); |
| } |
| } |
| |
| void IdleSpellCheckController::ContextDestroyed() { |
| Deactivate(); |
| } |
| |
| void IdleSpellCheckController::ForceInvocationForTesting() { |
| if (!IsSpellCheckingEnabled()) |
| return; |
| |
| auto* deadline = MakeGarbageCollected<IdleDeadline>( |
| base::TimeTicks::Now() + kIdleSpellcheckTestTimeout, |
| IdleDeadline::CallbackType::kCalledWhenIdle); |
| |
| switch (state_) { |
| case State::kColdModeTimerStarted: |
| cold_mode_timer_.Cancel(); |
| 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_.Cancel(); |
| ColdModeTimerFired(); |
| } |
| |
| void IdleSpellCheckController::SetNeedsMoreColdModeInvocationForTesting() { |
| cold_mode_requester_->SetNeedsMoreInvocationForTesting(); |
| } |
| |
| } // namespace blink |