1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383
|
// 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
|