Skip to content

Commit 05d9793

Browse files
authored
Extract a TextRange class for selection (flutter#21722)
Extracts a TextRange class with a base and extent, and start(), end(), collapsed(), and length() getters. The possibility of reversed base and extent in selections and composing ranges makes reasoning about them complex and increases the chances of errors in the code. This change migrates most uses of base and extent in the text model to start()/end() or position(). The position() method is intended purely as an aid to readability to indicate that a collapsed selection is expected at the call site; it also enforces a debug-time assertion that this is the case.
1 parent d912d50 commit 05d9793

File tree

5 files changed

+164
-78
lines changed

5 files changed

+164
-78
lines changed

shell/platform/common/cpp/BUILD.gn

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,19 @@ copy("publish_headers") {
3737
}
3838

3939
source_set("common_cpp_input") {
40-
public = [ "text_input_model.h" ]
40+
public = [
41+
"text_input_model.h",
42+
"text_range.h",
43+
]
4144

4245
sources = [ "text_input_model.cc" ]
4346

4447
configs += [ ":desktop_library_implementation" ]
4548

4649
public_configs = [ "//flutter:config" ]
4750

51+
deps = [ "//flutter/fml:fml" ]
52+
4853
if (is_win) {
4954
# For wstring_conversion. See issue #50053.
5055
defines = [ "_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING" ]
@@ -137,6 +142,7 @@ if (enable_unittests) {
137142
"json_message_codec_unittests.cc",
138143
"json_method_codec_unittests.cc",
139144
"text_input_model_unittests.cc",
145+
"text_range_unittests.cc",
140146
]
141147

142148
deps = [

shell/platform/common/cpp/text_input_model.cc

Lines changed: 45 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -38,27 +38,24 @@ void TextInputModel::SetText(const std::string& text) {
3838
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>
3939
utf16_converter;
4040
text_ = utf16_converter.from_bytes(text);
41-
selection_base_ = 0;
42-
selection_extent_ = 0;
41+
selection_ = TextRange(0);
4342
}
4443

4544
bool TextInputModel::SetSelection(size_t base, size_t extent) {
46-
auto max_pos = text_.length();
45+
size_t max_pos = text_.length();
4746
if (base > max_pos || extent > max_pos) {
4847
return false;
4948
}
50-
selection_base_ = base;
51-
selection_extent_ = extent;
49+
selection_ = TextRange(base, extent);
5250
return true;
5351
}
5452

5553
bool TextInputModel::DeleteSelected() {
56-
if (selection_base_ == selection_extent_) {
54+
if (selection_.collapsed()) {
5755
return false;
5856
}
59-
text_.erase(selection_start(), selection_end() - selection_start());
60-
selection_base_ = selection_start();
61-
selection_extent_ = selection_base_;
57+
text_.erase(selection_.start(), selection_.length());
58+
selection_ = TextRange(selection_.start());
6259
return true;
6360
}
6461

@@ -78,9 +75,9 @@ void TextInputModel::AddCodePoint(char32_t c) {
7875

7976
void TextInputModel::AddText(const std::u16string& text) {
8077
DeleteSelected();
81-
text_.insert(selection_extent_, text);
82-
selection_extent_ += text.length();
83-
selection_base_ = selection_extent_;
78+
size_t position = selection_.position();
79+
text_.insert(position, text);
80+
selection_ = TextRange(position + text.length());
8481
}
8582

8683
void TextInputModel::AddText(const std::string& text) {
@@ -90,38 +87,36 @@ void TextInputModel::AddText(const std::string& text) {
9087
}
9188

9289
bool TextInputModel::Backspace() {
93-
// If there's a selection, delete it.
9490
if (DeleteSelected()) {
9591
return true;
9692
}
97-
// There's no selection; delete the preceding codepoint.
98-
if (selection_base_ != 0) {
99-
int count = IsTrailingSurrogate(text_.at(selection_base_ - 1)) ? 2 : 1;
100-
text_.erase(selection_base_ - count, count);
101-
selection_base_ -= count;
102-
selection_extent_ = selection_base_;
93+
// If there's no selection, delete the preceding codepoint.
94+
size_t position = selection_.position();
95+
if (position != 0) {
96+
int count = IsTrailingSurrogate(text_.at(position - 1)) ? 2 : 1;
97+
text_.erase(position - count, count);
98+
selection_ = TextRange(position - count);
10399
return true;
104100
}
105101
return false;
106102
}
107103

108104
bool TextInputModel::Delete() {
109-
// If there's a selection, delete it.
110105
if (DeleteSelected()) {
111106
return true;
112107
}
113-
// There's no selection; delete the following codepoint.
114-
if (selection_base_ != text_.length()) {
115-
int count = IsLeadingSurrogate(text_.at(selection_base_)) ? 2 : 1;
116-
text_.erase(selection_base_, count);
117-
selection_extent_ = selection_base_;
108+
// If there's no selection, delete the preceding codepoint.
109+
size_t position = selection_.position();
110+
if (position != text_.length()) {
111+
int count = IsLeadingSurrogate(text_.at(position)) ? 2 : 1;
112+
text_.erase(position, count);
118113
return true;
119114
}
120115
return false;
121116
}
122117

123118
bool TextInputModel::DeleteSurrounding(int offset_from_cursor, int count) {
124-
auto start = selection_extent_;
119+
size_t start = selection_.extent();
125120
if (offset_from_cursor < 0) {
126121
for (int i = 0; i < -offset_from_cursor; i++) {
127122
// If requested start is before the available text then reduce the
@@ -150,65 +145,53 @@ bool TextInputModel::DeleteSurrounding(int offset_from_cursor, int count) {
150145
text_.erase(start, end - start);
151146

152147
// Cursor moves only if deleted area is before it.
153-
if (offset_from_cursor <= 0) {
154-
selection_base_ = start;
155-
}
156-
157-
// Clear selection.
158-
selection_extent_ = selection_base_;
148+
selection_ = TextRange(offset_from_cursor <= 0 ? start : selection_.start());
159149

160150
return true;
161151
}
162152

163153
bool TextInputModel::MoveCursorToBeginning() {
164-
if (selection_base_ == 0 && selection_extent_ == 0)
154+
if (selection_.collapsed() && selection_.position() == 0)
165155
return false;
166-
167-
selection_base_ = 0;
168-
selection_extent_ = 0;
156+
selection_ = TextRange(0);
169157
return true;
170158
}
171159

172160
bool TextInputModel::MoveCursorToEnd() {
173-
auto max_pos = text_.length();
174-
if (selection_base_ == max_pos && selection_extent_ == max_pos)
161+
size_t max_pos = text_.length();
162+
if (selection_.collapsed() && selection_.position() == max_pos)
175163
return false;
176-
177-
selection_base_ = max_pos;
178-
selection_extent_ = max_pos;
164+
selection_ = TextRange(max_pos);
179165
return true;
180166
}
181167

182168
bool TextInputModel::MoveCursorForward() {
183-
// If about to move set to the end of the highlight (when not selecting).
184-
if (selection_base_ != selection_extent_) {
185-
selection_base_ = selection_end();
186-
selection_extent_ = selection_base_;
169+
// If there's a selection, move to the end of the selection.
170+
if (!selection_.collapsed()) {
171+
selection_ = TextRange(selection_.end());
187172
return true;
188173
}
189-
// If not at the end, move the extent forward.
190-
if (selection_extent_ != text_.length()) {
191-
int count = IsLeadingSurrogate(text_.at(selection_base_)) ? 2 : 1;
192-
selection_base_ += count;
193-
selection_extent_ = selection_base_;
174+
// Otherwise, move the cursor forward.
175+
size_t position = selection_.position();
176+
if (position != text_.length()) {
177+
int count = IsLeadingSurrogate(text_.at(position)) ? 2 : 1;
178+
selection_ = TextRange(position + count);
194179
return true;
195180
}
196181
return false;
197182
}
198183

199184
bool TextInputModel::MoveCursorBack() {
200-
// If about to move set to the beginning of the highlight
201-
// (when not selecting).
202-
if (selection_base_ != selection_extent_) {
203-
selection_base_ = selection_start();
204-
selection_extent_ = selection_base_;
185+
// If there's a selection, move to the beginning of the selection.
186+
if (!selection_.collapsed()) {
187+
selection_ = TextRange(selection_.start());
205188
return true;
206189
}
207-
// If not at the start, move the beginning backward.
208-
if (selection_base_ != 0) {
209-
int count = IsTrailingSurrogate(text_.at(selection_base_ - 1)) ? 2 : 1;
210-
selection_base_ -= count;
211-
selection_extent_ = selection_base_;
190+
// Otherwise, move the cursor backward.
191+
size_t position = selection_.position();
192+
if (position != 0) {
193+
int count = IsTrailingSurrogate(text_.at(position - 1)) ? 2 : 1;
194+
selection_ = TextRange(position - count);
212195
return true;
213196
}
214197
return false;
@@ -221,9 +204,9 @@ std::string TextInputModel::GetText() const {
221204
}
222205

223206
int TextInputModel::GetCursorOffset() const {
224-
// Measure the length of the current text up to the cursor.
207+
// Measure the length of the current text up to the selection extent.
225208
// There is probably a much more efficient way of doing this.
226-
auto leading_text = text_.substr(0, selection_extent_);
209+
auto leading_text = text_.substr(0, selection_.extent());
227210
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t>
228211
utf8_converter;
229212
return utf8_converter.to_bytes(leading_text).size();

shell/platform/common/cpp/text_input_model.h

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
#ifndef FLUTTER_SHELL_PLATFORM_CPP_TEXT_INPUT_MODEL_H_
66
#define FLUTTER_SHELL_PLATFORM_CPP_TEXT_INPUT_MODEL_H_
77

8-
#include <algorithm>
98
#include <memory>
109
#include <string>
1110

11+
#include "flutter/shell/platform/common/cpp/text_range.h"
12+
1213
namespace flutter {
14+
1315
// Handles underlying text input state, using a simple ASCII model.
1416
//
1517
// Ignores special states like "insert mode" for now.
@@ -104,10 +106,10 @@ class TextInputModel {
104106
int GetCursorOffset() const;
105107

106108
// The position where the selection starts.
107-
int selection_base() const { return selection_base_; }
109+
int selection_base() const { return selection_.base(); }
108110

109111
// The position of the cursor.
110-
int selection_extent() const { return selection_extent_; }
112+
int selection_extent() const { return selection_.extent(); }
111113

112114
private:
113115
// Deletes the current selection, if any.
@@ -117,18 +119,7 @@ class TextInputModel {
117119
bool DeleteSelected();
118120

119121
std::u16string text_;
120-
size_t selection_base_ = 0;
121-
size_t selection_extent_ = 0;
122-
123-
// Returns the left hand side of the selection.
124-
size_t selection_start() const {
125-
return std::min(selection_base_, selection_extent_);
126-
}
127-
128-
// Returns the right hand side of the selection.
129-
size_t selection_end() const {
130-
return std::max(selection_base_, selection_extent_);
131-
}
122+
TextRange selection_ = TextRange(0);
132123
};
133124

134125
} // namespace flutter
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#include <algorithm>
6+
7+
#include "flutter/fml/logging.h"
8+
9+
// A directional range of text.
10+
//
11+
// A |TextRange| describes a range of text with |base| and |extent| positions.
12+
// In the case where |base| == |extent|, the range is said to be collapsed, and
13+
// when |base| > |extent|, the range is said to be reversed.
14+
class TextRange {
15+
public:
16+
explicit TextRange(size_t position) : base_(position), extent_(position) {}
17+
explicit TextRange(size_t base, size_t extent)
18+
: base_(base), extent_(extent) {}
19+
TextRange(const TextRange&) = default;
20+
TextRange& operator=(const TextRange&) = default;
21+
22+
virtual ~TextRange() = default;
23+
24+
// Returns the base position of the range.
25+
size_t base() const { return base_; }
26+
27+
// Returns the extent position of the range.
28+
size_t extent() const { return extent_; }
29+
30+
// Returns the lesser of the base and extent positions.
31+
size_t start() const { return std::min(base_, extent_); }
32+
33+
// Returns the greater of the base and extent positions.
34+
size_t end() const { return std::max(base_, extent_); }
35+
36+
// Returns the position of a collapsed range.
37+
//
38+
// Asserts that the range is of length 0.
39+
size_t position() const {
40+
FML_DCHECK(base_ == extent_);
41+
return extent_;
42+
}
43+
44+
// Returns the length of the range.
45+
size_t length() const { return end() - start(); }
46+
47+
// Returns true if the range is of length 0.
48+
bool collapsed() const { return base_ == extent_; }
49+
50+
private:
51+
size_t base_;
52+
size_t extent_;
53+
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#include "flutter/shell/platform/common/cpp/text_range.h"
6+
7+
#include "gtest/gtest.h"
8+
9+
namespace flutter {
10+
11+
TEST(TextRange, TextRangeFromPositionZero) {
12+
TextRange range(0);
13+
EXPECT_EQ(range.base(), size_t(0));
14+
EXPECT_EQ(range.extent(), size_t(0));
15+
EXPECT_EQ(range.start(), size_t(0));
16+
EXPECT_EQ(range.end(), size_t(0));
17+
EXPECT_EQ(range.length(), size_t(0));
18+
EXPECT_EQ(range.position(), size_t(0));
19+
EXPECT_TRUE(range.collapsed());
20+
}
21+
22+
TEST(TextRange, TextRangeFromPositionNonZero) {
23+
TextRange range(3);
24+
EXPECT_EQ(range.base(), size_t(3));
25+
EXPECT_EQ(range.extent(), size_t(3));
26+
EXPECT_EQ(range.start(), size_t(3));
27+
EXPECT_EQ(range.end(), size_t(3));
28+
EXPECT_EQ(range.length(), size_t(0));
29+
EXPECT_EQ(range.position(), size_t(3));
30+
EXPECT_TRUE(range.collapsed());
31+
}
32+
33+
TEST(TextRange, TextRangeFromRange) {
34+
TextRange range(3, 7);
35+
EXPECT_EQ(range.base(), size_t(3));
36+
EXPECT_EQ(range.extent(), size_t(7));
37+
EXPECT_EQ(range.start(), size_t(3));
38+
EXPECT_EQ(range.end(), size_t(7));
39+
EXPECT_EQ(range.length(), size_t(4));
40+
EXPECT_FALSE(range.collapsed());
41+
}
42+
43+
TEST(TextRange, TextRangeFromReversedRange) {
44+
TextRange range(7, 3);
45+
EXPECT_EQ(range.base(), size_t(7));
46+
EXPECT_EQ(range.extent(), size_t(3));
47+
EXPECT_EQ(range.start(), size_t(3));
48+
EXPECT_EQ(range.end(), size_t(7));
49+
EXPECT_EQ(range.length(), size_t(4));
50+
EXPECT_FALSE(range.collapsed());
51+
}
52+
53+
} // namespace flutter

0 commit comments

Comments
 (0)