LibUnicode: Avoid rejecting end-of-text position as a valid boundary

When the cursor was positioned at the end of text,
attempting to move it left(using the left arrow key)
would fail because align_boundary() was rejecting
the end-of-text position as a valid boundary.
This commit is contained in:
mikiubo 2025-04-07 23:13:31 +02:00 committed by Tim Flynn
commit 8ec72d6906
Notes: github-actions[bot] 2025-04-11 19:31:19 +00:00
2 changed files with 38 additions and 18 deletions

View file

@ -87,15 +87,13 @@ public:
virtual Optional<size_t> previous_boundary(size_t boundary, Inclusive inclusive) override virtual Optional<size_t> previous_boundary(size_t boundary, Inclusive inclusive) override
{ {
auto icu_boundary = align_boundary(boundary); auto icu_boundary = align_boundary(boundary);
if (!icu_boundary.has_value())
return {};
if (inclusive == Inclusive::Yes) { if (inclusive == Inclusive::Yes) {
if (static_cast<bool>(m_segmenter->isBoundary(*icu_boundary))) if (static_cast<bool>(m_segmenter->isBoundary(icu_boundary)))
return static_cast<size_t>(*icu_boundary); return static_cast<size_t>(icu_boundary);
} }
if (auto index = m_segmenter->preceding(*icu_boundary); index != icu::BreakIterator::DONE) if (auto index = m_segmenter->preceding(icu_boundary); index != icu::BreakIterator::DONE)
return static_cast<size_t>(index); return static_cast<size_t>(index);
return {}; return {};
@ -104,15 +102,13 @@ public:
virtual Optional<size_t> next_boundary(size_t boundary, Inclusive inclusive) override virtual Optional<size_t> next_boundary(size_t boundary, Inclusive inclusive) override
{ {
auto icu_boundary = align_boundary(boundary); auto icu_boundary = align_boundary(boundary);
if (!icu_boundary.has_value())
return {};
if (inclusive == Inclusive::Yes) { if (inclusive == Inclusive::Yes) {
if (static_cast<bool>(m_segmenter->isBoundary(*icu_boundary))) if (static_cast<bool>(m_segmenter->isBoundary(icu_boundary)))
return static_cast<size_t>(*icu_boundary); return static_cast<size_t>(icu_boundary);
} }
if (auto index = m_segmenter->following(*icu_boundary); index != icu::BreakIterator::DONE) if (auto index = m_segmenter->following(icu_boundary); index != icu::BreakIterator::DONE)
return static_cast<size_t>(index); return static_cast<size_t>(index);
return {}; return {};
@ -177,25 +173,25 @@ public:
} }
private: private:
Optional<i32> align_boundary(size_t boundary) i32 align_boundary(size_t boundary)
{ {
auto icu_boundary = static_cast<i32>(boundary); auto icu_boundary = static_cast<i32>(boundary);
return m_segmented_text.visit( return m_segmented_text.visit(
[&](String const& text) -> Optional<i32> { [&](String const& text) {
if (boundary >= text.byte_count()) if (boundary >= text.byte_count())
return {}; return static_cast<i32>(text.byte_count());
U8_SET_CP_START(text.bytes().data(), 0, icu_boundary); U8_SET_CP_START(text.bytes().data(), 0, icu_boundary);
return icu_boundary; return icu_boundary;
}, },
[&](icu::UnicodeString const& text) -> Optional<i32> { [&](icu::UnicodeString const& text) {
if (icu_boundary >= text.length()) if (icu_boundary >= text.length())
return {}; return text.length();
return text.getChar32Start(icu_boundary); return text.getChar32Start(icu_boundary);
}, },
[](Empty) -> Optional<i32> { VERIFY_NOT_REACHED(); }); [](Empty) -> i32 { VERIFY_NOT_REACHED(); });
} }
void for_each_boundary(SegmentationCallback callback) void for_each_boundary(SegmentationCallback callback)

View file

@ -136,11 +136,23 @@ TEST_CASE(out_of_bounds)
auto segmenter = Unicode::Segmenter::create(Unicode::SegmenterGranularity::Word); auto segmenter = Unicode::Segmenter::create(Unicode::SegmenterGranularity::Word);
segmenter->set_segmented_text(text); segmenter->set_segmented_text(text);
auto result = segmenter->previous_boundary(text.byte_count()); auto result = segmenter->previous_boundary(text.byte_count() + 1);
EXPECT(result.has_value());
result = segmenter->next_boundary(text.byte_count() + 1);
EXPECT(!result.has_value()); EXPECT(!result.has_value());
result = segmenter->previous_boundary(text.byte_count());
EXPECT(result.has_value());
result = segmenter->next_boundary(text.byte_count()); result = segmenter->next_boundary(text.byte_count());
EXPECT(!result.has_value()); EXPECT(!result.has_value());
result = segmenter->next_boundary(0);
EXPECT(result.has_value());
result = segmenter->previous_boundary(0);
EXPECT(!result.has_value());
} }
{ {
auto text = MUST(AK::utf8_to_utf16("foo"sv)); auto text = MUST(AK::utf8_to_utf16("foo"sv));
@ -148,10 +160,22 @@ TEST_CASE(out_of_bounds)
auto segmenter = Unicode::Segmenter::create(Unicode::SegmenterGranularity::Word); auto segmenter = Unicode::Segmenter::create(Unicode::SegmenterGranularity::Word);
segmenter->set_segmented_text(Utf16View { text }); segmenter->set_segmented_text(Utf16View { text });
auto result = segmenter->previous_boundary(text.size()); auto result = segmenter->previous_boundary(text.size() + 1);
EXPECT(result.has_value());
result = segmenter->next_boundary(text.size() + 1);
EXPECT(!result.has_value()); EXPECT(!result.has_value());
result = segmenter->previous_boundary(text.size());
EXPECT(result.has_value());
result = segmenter->next_boundary(text.size()); result = segmenter->next_boundary(text.size());
EXPECT(!result.has_value()); EXPECT(!result.has_value());
result = segmenter->next_boundary(0);
EXPECT(result.has_value());
result = segmenter->previous_boundary(0);
EXPECT(!result.has_value());
} }
} }