blob: 4cdc70e0f6a94578f76941e21a2c86d9aa1ce933 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/base/ime/surrounding_text_tracker.h"
#include <string_view>
#include <utility>
#include "base/check.h"
#include "base/logging.h"
#include "base/notreached.h"
namespace ui {
namespace {
// Replaces the substring of the given str with the offset with replacement.
// If the range is outside of the str, str will not be updated (i.e. final
// result may not contain replacement) but at least offset should be adjusted.
void ReplaceString16WithOffset(std::u16string& str,
size_t& offset,
size_t pos,
size_t n,
const std::u16string& replacement) {
if (offset + str.length() < pos) {
// replacement starts after the known str. Do nothing.
return;
}
if (pos + n < offset) {
// replacement is in the range of [0:offset). Just adjust the offset.
offset += replacement.length() - n;
return;
}
// Here, we have overlap (including just concatenating) of the original
// str and the replacement.
if (pos < offset) {
// Replacement starts before the current offset.
// Merge the pattern, and adjust the offset.
str.replace(0, n - (offset - pos), replacement);
offset = pos;
return;
}
// Here, the overlap starts the same as or after the offset.
// In this case, offset is not changed.
size_t begin = pos - offset; // Begin index within str.
str.replace(begin, std::min(n, str.length() - begin), replacement);
}
// Erases [pos:pos+n) from the given str with the offset.
void EraseString16WithOffset(std::u16string& str,
size_t& offset,
size_t pos,
size_t n) {
if (offset + str.length() <= pos) {
// The erasing range is after the str's range. Do nothing.
return;
}
if (pos + n <= offset) {
// The erasing range is included in [0:offset]. Just adjust the offset.
offset -= n;
return;
}
// Here we have to actually erase some range of str.
if (pos < offset) {
// The erasing range starts before the offset.
str.erase(0, n - (offset - pos));
offset = pos;
return;
}
size_t begin = pos - offset;
str.erase(begin, std::min(n, str.length() - begin));
}
} // namespace
gfx::Range SurroundingTextTracker::State::GetSurroundingTextRange() const {
return {utf16_offset, utf16_offset + surrounding_text.length()};
}
std::optional<std::u16string_view>
SurroundingTextTracker::State::GetCompositionText() const {
if (composition.is_empty()) {
// Represents no composition. Return empty composition text as a valid
// result.
return std::u16string_view();
}
if (!composition.IsBoundedBy(GetSurroundingTextRange())) {
// composition range is out of the range. Return error.
return std::nullopt;
}
return std::u16string_view(surrounding_text)
.substr(composition.GetMin() - utf16_offset, composition.length());
}
SurroundingTextTracker::Entry::Entry(State state,
base::RepeatingClosure command)
: state(std::move(state)), command(std::move(command)) {}
SurroundingTextTracker::Entry::Entry(const Entry&) = default;
SurroundingTextTracker::Entry::Entry(Entry&&) = default;
SurroundingTextTracker::Entry& SurroundingTextTracker::Entry::operator=(
const Entry&) = default;
SurroundingTextTracker::Entry& SurroundingTextTracker::Entry::operator=(
Entry&&) = default;
SurroundingTextTracker::Entry::~Entry() = default;
SurroundingTextTracker::SurroundingTextTracker() {
ResetInternal(u"", 0u, gfx::Range(0));
}
SurroundingTextTracker::~SurroundingTextTracker() = default;
void SurroundingTextTracker::Reset() {
ResetInternal(u"", 0u, gfx::Range(0));
}
void SurroundingTextTracker::CancelComposition() {
predicted_state_.composition = gfx::Range();
// TODO(b/267944900): Determine if the expectations need to be updated as
// well.
expected_updates_.clear();
}
SurroundingTextTracker::UpdateResult SurroundingTextTracker::Update(
const std::u16string_view surrounding_text,
size_t utf16_offset,
const gfx::Range& selection) {
for (auto it = expected_updates_.begin(); it != expected_updates_.end();
++it) {
if (it->state.selection != selection) {
continue;
}
// TODO(crbug.com/40251329): Limit the trailing text to support cases
// where trailing text is truncated.
size_t compare_begin = std::max(utf16_offset, it->state.utf16_offset);
std::u16string_view target =
surrounding_text.substr(compare_begin - utf16_offset);
std::u16string_view history =
std::u16string_view(it->state.surrounding_text)
.substr(compare_begin - it->state.utf16_offset);
if (target != history) {
continue;
}
// Found the target state, but it may be different from the one we
// estimate. Because the Update may be called multiple times for the same
// event. Check if the recorded state is exact same here to skip unneeded
// recalculation.
if (it->state.surrounding_text == surrounding_text &&
it->state.utf16_offset == utf16_offset &&
it->state.selection == selection) {
expected_updates_.erase(expected_updates_.begin(), it);
return UpdateResult::kUpdated;
}
// Otherwise, recalculate the predicts.
predicted_state_ = State{
std::u16string(surrounding_text), utf16_offset, selection,
predicted_state_.composition, // Carried from the original state.
};
base::RepeatingClosure current_command = std::move(it->command);
std::vector<base::RepeatingClosure> remaining_commands;
for (++it; it != expected_updates_.end(); ++it) {
remaining_commands.push_back(std::move(it->command));
}
expected_updates_.clear();
expected_updates_.emplace_back(predicted_state_,
std::move(current_command));
// Replay all remaining commands to re-calculate predicted states from the
// given one.
for (auto& command : remaining_commands) {
command.Run();
}
return UpdateResult::kUpdated;
}
VLOG(1) << "Unknown surrounding text update is found";
ResetInternal(surrounding_text, utf16_offset, selection);
return UpdateResult::kReset;
}
void SurroundingTextTracker::OnSetEditableSelectionRange(
const gfx::Range& range) {
predicted_state_.selection = range;
expected_updates_.emplace_back(
predicted_state_,
base::BindRepeating(&SurroundingTextTracker::OnSetEditableSelectionRange,
base::Unretained(this), range));
}
void SurroundingTextTracker::OnSetCompositionText(
const ui::CompositionText& composition) {
// If it has a composition text already, replace it.
// Otherwise, replace (or insert) selected text.
const gfx::Range& old_range = predicted_state_.composition.is_empty()
? predicted_state_.selection
: predicted_state_.composition;
size_t composition_begin = old_range.GetMin();
if (old_range.GetMax() < predicted_state_.utf16_offset ||
old_range.GetMin() > predicted_state_.utf16_offset +
predicted_state_.surrounding_text.length()) {
predicted_state_.surrounding_text = composition.text;
predicted_state_.utf16_offset = composition_begin;
} else {
ReplaceString16WithOffset(predicted_state_.surrounding_text,
predicted_state_.utf16_offset, composition_begin,
old_range.length(), composition.text);
}
predicted_state_.selection =
gfx::Range(composition_begin + composition.selection.start(),
composition_begin + composition.selection.end());
predicted_state_.composition = gfx::Range(
composition_begin, composition_begin + composition.text.length());
expected_updates_.emplace_back(
predicted_state_,
base::BindRepeating(&SurroundingTextTracker::OnSetCompositionText,
base::Unretained(this), composition));
}
void SurroundingTextTracker::OnSetCompositionFromExistingText(
const gfx::Range& range) {
predicted_state_.composition = range;
expected_updates_.emplace_back(
predicted_state_,
base::BindRepeating(
&SurroundingTextTracker::OnSetCompositionFromExistingText,
base::Unretained(this), range));
}
void SurroundingTextTracker::OnConfirmCompositionText(bool keep_selection) {
if (!predicted_state_.composition.is_empty()) {
if (!keep_selection && !predicted_state_.composition.is_empty()) {
predicted_state_.selection =
gfx::Range(predicted_state_.composition.end());
}
predicted_state_.composition = gfx::Range();
}
expected_updates_.emplace_back(
predicted_state_,
base::BindRepeating(&SurroundingTextTracker::OnConfirmCompositionText,
base::Unretained(this), keep_selection));
}
void SurroundingTextTracker::OnClearCompositionText() {
if (!predicted_state_.composition.is_empty()) {
EraseString16WithOffset(predicted_state_.surrounding_text,
predicted_state_.utf16_offset,
predicted_state_.composition.GetMin(),
predicted_state_.composition.length());
// Set selection to the position where composition existed.
predicted_state_.selection =
gfx::Range(predicted_state_.composition.GetMin());
predicted_state_.composition = gfx::Range();
}
expected_updates_.emplace_back(
predicted_state_,
base::BindRepeating(&SurroundingTextTracker::OnClearCompositionText,
base::Unretained(this)));
}
void SurroundingTextTracker::OnInsertText(
const std::u16string_view text,
TextInputClient::InsertTextCursorBehavior cursor_behavior) {
gfx::Range rewritten_range = predicted_state_.selection;
if (!predicted_state_.composition.is_empty()) {
// Cancel the current composition.
if (predicted_state_.composition.Intersects(rewritten_range)) {
// Selection and composition has overlap, so take the union here.
// Just after this section, the whole range will be replaced by |text|.
rewritten_range =
gfx::Range(std::min(predicted_state_.composition.GetMin(),
rewritten_range.GetMin()),
std::max(predicted_state_.composition.GetMax(),
rewritten_range.GetMax()));
} else {
// Otherwise, remove the composition. If the composition appears before
// the rewritten range, the offset needs to be updated.
EraseString16WithOffset(predicted_state_.surrounding_text,
predicted_state_.utf16_offset,
predicted_state_.composition.GetMin(),
predicted_state_.composition.length());
if (rewritten_range.GetMin() > predicted_state_.composition.GetMin()) {
rewritten_range = gfx::Range(
rewritten_range.start() - predicted_state_.composition.length(),
rewritten_range.end() - predicted_state_.composition.length());
}
}
}
if (rewritten_range.GetMin() >
predicted_state_.utf16_offset +
predicted_state_.surrounding_text.length() ||
rewritten_range.GetMax() < predicted_state_.utf16_offset) {
predicted_state_.surrounding_text = std::u16string(text);
predicted_state_.utf16_offset = rewritten_range.GetMin();
} else {
ReplaceString16WithOffset(predicted_state_.surrounding_text,
predicted_state_.utf16_offset,
rewritten_range.GetMin(),
rewritten_range.length(), std::u16string(text));
}
predicted_state_.selection =
cursor_behavior ==
TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText
? gfx::Range(rewritten_range.GetMin() + text.length())
: gfx::Range(rewritten_range.GetMin());
predicted_state_.composition = gfx::Range();
expected_updates_.emplace_back(
predicted_state_,
base::BindRepeating(&SurroundingTextTracker::OnInsertText,
// Bind `text` as a `std::u16string` to avoid
// a dangling string_view when callbacks are run.
base::Unretained(this), std::u16string(text),
cursor_behavior));
}
void SurroundingTextTracker::OnExtendSelectionAndDelete(size_t before,
size_t after) {
if (before != 0 || after != 0 || !predicted_state_.selection.is_empty() ||
!predicted_state_.composition.is_empty()) {
gfx::Range delete_range(
predicted_state_.selection.GetMin() -
std::min(before, predicted_state_.selection.GetMin()),
predicted_state_.selection.GetMax() + after);
if (!predicted_state_.composition.is_empty()) {
// Cancel the current composition.
if (predicted_state_.composition.Intersects(delete_range)) {
// Expand the delete_range to include the whole composition range,
// if there's some overlap.
delete_range =
gfx::Range(std::min(predicted_state_.composition.GetMin(),
delete_range.GetMin()),
std::max(predicted_state_.composition.GetMax(),
delete_range.GetMax()));
} else {
// Otherwise, remove the composition here. If the composition appears
// before the delete_range, the offset needs to be updated.
EraseString16WithOffset(predicted_state_.surrounding_text,
predicted_state_.utf16_offset,
predicted_state_.composition.GetMin(),
predicted_state_.composition.length());
if (delete_range.GetMin() > predicted_state_.composition.GetMin()) {
delete_range = gfx::Range(
delete_range.start() - predicted_state_.composition.length(),
delete_range.end() - predicted_state_.composition.length());
}
}
}
EraseString16WithOffset(predicted_state_.surrounding_text,
predicted_state_.utf16_offset,
delete_range.GetMin(), delete_range.length());
predicted_state_.selection = gfx::Range(delete_range.GetMin());
predicted_state_.composition = gfx::Range();
}
expected_updates_.emplace_back(
predicted_state_,
base::BindRepeating(&SurroundingTextTracker::OnExtendSelectionAndDelete,
base::Unretained(this), before, after));
}
void SurroundingTextTracker::ResetInternal(std::u16string_view surrounding_text,
size_t utf16_offset,
const gfx::Range& selection) {
predicted_state_ = State{std::u16string(surrounding_text), utf16_offset,
selection, gfx::Range()};
expected_updates_.clear();
expected_updates_.emplace_back(predicted_state_, base::RepeatingClosure());
}
} // namespace ui