AK+UI: Stop using StringBuilder UTF-16 mode

Move the remaining call sites that constructed StringBuilder in UTF-16
mode over to Utf16StringBuilder or explicit UTF-8 to UTF-16 conversion.
Teach FormatBuilder to target Utf16StringBuilder directly so formatted
Utf16String construction no longer needs StringBuilder's UTF-16 mode.

Update the Utf16String tests to cover Utf16StringBuilder adoption and
clear behavior directly.
This commit is contained in:
Andreas Kling 2026-06-22 14:23:59 +02:00 committed by Andreas Kling
parent 26b0801a78
commit a8193e7572
9 changed files with 114 additions and 70 deletions

View file

@ -217,16 +217,40 @@ bool FormatParser::consume_replacement_field(size_t& index)
return true; return true;
} }
ErrorOr<void> FormatBuilder::append(char ch)
{
if (m_string_builder)
return m_string_builder->try_append(ch);
m_utf16_builder->append_ascii(ch);
return {};
}
ErrorOr<void> FormatBuilder::append(StringView string)
{
if (m_string_builder)
return m_string_builder->try_append(string);
m_utf16_builder->append(Utf16String::from_utf8_without_validation(string));
return {};
}
ErrorOr<void> FormatBuilder::append(Utf16View const& string)
{
if (m_string_builder)
return m_string_builder->try_append(string);
m_utf16_builder->append(string);
return {};
}
ErrorOr<void> FormatBuilder::put_padding(char fill, size_t amount) ErrorOr<void> FormatBuilder::put_padding(char fill, size_t amount)
{ {
for (size_t i = 0; i < amount; ++i) for (size_t i = 0; i < amount; ++i)
TRY(m_builder.try_append(fill)); TRY(append(fill));
return {}; return {};
} }
ErrorOr<void> FormatBuilder::put_literal(StringView value) ErrorOr<void> FormatBuilder::put_literal(StringView value)
{ {
for (size_t i = 0; i < value.length(); ++i) { for (size_t i = 0; i < value.length(); ++i) {
TRY(m_builder.try_append(value[i])); TRY(append(value[i]));
if (value[i] == '{' || value[i] == '}') if (value[i] == '{' || value[i] == '}')
++i; ++i;
} }
@ -247,22 +271,28 @@ ErrorOr<void> FormatBuilder::put_string(
value = value.substring_view(0, used_by_string); value = value.substring_view(0, used_by_string);
if (align == Align::Left || align == Align::Default) { if (align == Align::Left || align == Align::Default) {
TRY(m_builder.try_append(value)); TRY(append(value));
TRY(put_padding(fill, used_by_padding)); TRY(put_padding(fill, used_by_padding));
} else if (align == Align::Center) { } else if (align == Align::Center) {
auto const used_by_left_padding = used_by_padding / 2; auto const used_by_left_padding = used_by_padding / 2;
auto const used_by_right_padding = ceil_div<size_t, size_t>(used_by_padding, 2); auto const used_by_right_padding = ceil_div<size_t, size_t>(used_by_padding, 2);
TRY(put_padding(fill, used_by_left_padding)); TRY(put_padding(fill, used_by_left_padding));
TRY(m_builder.try_append(value)); TRY(append(value));
TRY(put_padding(fill, used_by_right_padding)); TRY(put_padding(fill, used_by_right_padding));
} else if (align == Align::Right) { } else if (align == Align::Right) {
TRY(put_padding(fill, used_by_padding)); TRY(put_padding(fill, used_by_padding));
TRY(m_builder.try_append(value)); TRY(append(value));
} }
return {}; return {};
} }
ErrorOr<void> FormatBuilder::put_string(Utf16View const& value)
{
TRY(append(value));
return {};
}
ErrorOr<void> FormatBuilder::put_u64( ErrorOr<void> FormatBuilder::put_u64(
u64 value, u64 value,
u8 base, u8 base,
@ -307,25 +337,25 @@ ErrorOr<void> FormatBuilder::put_u64(
auto const put_prefix = [&]() -> ErrorOr<void> { auto const put_prefix = [&]() -> ErrorOr<void> {
if (is_negative) if (is_negative)
TRY(m_builder.try_append('-')); TRY(append('-'));
else if (sign_mode == SignMode::Always) else if (sign_mode == SignMode::Always)
TRY(m_builder.try_append('+')); TRY(append('+'));
else if (sign_mode == SignMode::Reserved) else if (sign_mode == SignMode::Reserved)
TRY(m_builder.try_append(' ')); TRY(append(' '));
if (prefix) { if (prefix) {
if (base == 2) { if (base == 2) {
if (upper_case) if (upper_case)
TRY(m_builder.try_append("0B"sv)); TRY(append("0B"sv));
else else
TRY(m_builder.try_append("0b"sv)); TRY(append("0b"sv));
} else if (base == 8) { } else if (base == 8) {
TRY(m_builder.try_append("0"sv)); TRY(append("0"sv));
} else if (base == 16) { } else if (base == 16) {
if (upper_case) if (upper_case)
TRY(m_builder.try_append("0X"sv)); TRY(append("0X"sv));
else else
TRY(m_builder.try_append("0x"sv)); TRY(append("0x"sv));
} }
} }
return {}; return {};
@ -333,7 +363,7 @@ ErrorOr<void> FormatBuilder::put_u64(
auto const put_digits = [&]() -> ErrorOr<void> { auto const put_digits = [&]() -> ErrorOr<void> {
for (size_t i = 0; i < used_by_digits; ++i) for (size_t i = 0; i < used_by_digits; ++i)
TRY(m_builder.try_append(buffer[i])); TRY(append(buffer[i]));
return {}; return {};
}; };
@ -795,7 +825,7 @@ ErrorOr<void> FormatBuilder::put_hexdump(ReadonlyBytes bytes, size_t width, char
TRY(put_padding(fill, 4)); TRY(put_padding(fill, 4));
for (size_t j = i - min(i, width); j < i; ++j) { for (size_t j = i - min(i, width); j < i; ++j) {
auto ch = bytes[j]; auto ch = bytes[j];
TRY(m_builder.try_append(ch >= 32 && ch <= 127 ? ch : '.')); // silly hack TRY(append(ch >= 32 && ch <= 127 ? ch : '.')); // silly hack
} }
return {}; return {};
}; };
@ -827,9 +857,10 @@ ErrorOr<void> vformat(StringBuilder& builder, StringView fmtstr, TypeErasedForma
ErrorOr<void> vformat(Utf16StringBuilder& builder, StringView fmtstr, TypeErasedFormatParams& params) ErrorOr<void> vformat(Utf16StringBuilder& builder, StringView fmtstr, TypeErasedFormatParams& params)
{ {
StringBuilder string_builder(StringBuilder::Mode::UTF16); FormatBuilder fmtbuilder { builder };
TRY(vformat(string_builder, fmtstr, params)); FormatParser parser { fmtstr };
builder.append(string_builder.to_utf16_string());
TRY(vformat_impl(params, fmtbuilder, parser));
return {}; return {};
} }

View file

@ -217,7 +217,12 @@ public:
}; };
explicit FormatBuilder(StringBuilder& builder) explicit FormatBuilder(StringBuilder& builder)
: m_builder(builder) : m_string_builder(&builder)
{
}
explicit FormatBuilder(Utf16StringBuilder& builder)
: m_utf16_builder(&builder)
{ {
} }
@ -231,6 +236,7 @@ public:
size_t min_width = 0, size_t min_width = 0,
size_t max_width = NumericLimits<size_t>::max(), size_t max_width = NumericLimits<size_t>::max(),
char fill = ' '); char fill = ' ');
ErrorOr<void> put_string(Utf16View const& value);
ErrorOr<void> put_u64( ErrorOr<void> put_u64(
u64 value, u64 value,
@ -306,12 +312,22 @@ public:
StringBuilder const& builder() const StringBuilder const& builder() const
{ {
return m_builder; VERIFY(m_string_builder);
return *m_string_builder;
}
StringBuilder& builder()
{
VERIFY(m_string_builder);
return *m_string_builder;
} }
StringBuilder& builder() { return m_builder; }
private: private:
StringBuilder& m_builder; ErrorOr<void> append(char);
ErrorOr<void> append(StringView);
ErrorOr<void> append(Utf16View const&);
StringBuilder* m_string_builder { nullptr };
Utf16StringBuilder* m_utf16_builder { nullptr };
ErrorOr<void> put_f64_with_precision( ErrorOr<void> put_f64_with_precision(
double value, double value,

View file

@ -703,9 +703,9 @@ ErrorOr<String> UnixDateTime::to_string(StringView format, LocalTime local_time)
Utf16String UnixDateTime::to_utf16_string(StringView format, LocalTime local_time) const Utf16String UnixDateTime::to_utf16_string(StringView format, LocalTime local_time) const
{ {
StringBuilder builder(StringBuilder::Mode::UTF16); StringBuilder builder;
MUST(to_string_impl(builder, format, local_time)); MUST(to_string_impl(builder, format, local_time));
return builder.to_utf16_string(); return Utf16String::from_utf8_without_validation(MUST(builder.to_string()));
} }
ByteString UnixDateTime::to_byte_string(StringView format, LocalTime local_time) const ByteString UnixDateTime::to_byte_string(StringView format, LocalTime local_time) const

View file

@ -25,7 +25,7 @@ Utf16String Utf16String::from_utf8_with_replacement_character(StringView utf8_st
if (utf8_view.validate(AllowLonelySurrogates::No)) if (utf8_view.validate(AllowLonelySurrogates::No))
return Utf16String::from_utf8_without_validation(utf8_string); return Utf16String::from_utf8_without_validation(utf8_string);
StringBuilder builder(StringBuilder::Mode::UTF16); Utf16StringBuilder builder;
for (auto code_point : utf8_view) { for (auto code_point : utf8_view) {
if (is_unicode_surrogate(code_point)) if (is_unicode_surrogate(code_point))
@ -34,7 +34,7 @@ Utf16String Utf16String::from_utf8_with_replacement_character(StringView utf8_st
builder.append_code_point(code_point); builder.append_code_point(code_point);
} }
return builder.to_utf16_string(); return builder.to_string();
} }
Utf16String Utf16String::from_ascii_without_validation(ReadonlyBytes ascii_string) Utf16String Utf16String::from_ascii_without_validation(ReadonlyBytes ascii_string)
@ -158,9 +158,9 @@ Utf16String Utf16String::repeated(u32 code_point, size_t count)
code_units[length_in_code_units++] = code_unit; code_units[length_in_code_units++] = code_unit;
}); });
StringBuilder builder(StringBuilder::Mode::UTF16); Utf16StringBuilder builder;
builder.append_repeated({ code_units.data(), length_in_code_units }, count); builder.append_repeated({ code_units.data(), length_in_code_units }, count);
return builder.to_utf16_string(); return builder.to_string();
} }
Utf16String Utf16String::to_well_formed() const Utf16String Utf16String::to_well_formed() const
@ -180,7 +180,7 @@ String Utf16String::to_well_formed_utf8() const
ErrorOr<void> Formatter<Utf16String>::format(FormatBuilder& builder, Utf16String const& utf16_string) ErrorOr<void> Formatter<Utf16String>::format(FormatBuilder& builder, Utf16String const& utf16_string)
{ {
if (utf16_string.has_long_utf16_storage()) if (utf16_string.has_long_utf16_storage())
return builder.builder().try_append(utf16_string.utf16_view()); return builder.put_string(utf16_string.utf16_view());
return builder.put_string(utf16_string.ascii_view()); return builder.put_string(utf16_string.ascii_view());
} }

View file

@ -134,10 +134,16 @@ public:
template<class SeparatorType, class CollectionType> template<class SeparatorType, class CollectionType>
ALWAYS_INLINE static Utf16String join(SeparatorType const& separator, CollectionType const& collection, StringView format = "{}"sv) ALWAYS_INLINE static Utf16String join(SeparatorType const& separator, CollectionType const& collection, StringView format = "{}"sv)
{ {
StringBuilder builder(StringBuilder::Mode::UTF16); Utf16StringBuilder builder;
builder.join(separator, collection, format); bool first = true;
for (auto& item : collection) {
if (!first)
builder.appendff("{}", separator);
builder.appendff(format, item);
first = false;
}
return builder.to_utf16_string(); return builder.to_string();
} }
static Utf16String repeated(u32 code_point, size_t count); static Utf16String repeated(u32 code_point, size_t count);

View file

@ -59,27 +59,27 @@ ErrorOr<ByteString> Utf16View::to_byte_string(AllowLonelySurrogates allow_lonely
Utf16String Utf16View::to_ascii_lowercase() const Utf16String Utf16View::to_ascii_lowercase() const
{ {
StringBuilder builder(StringBuilder::Mode::UTF16, length_in_code_units()); Utf16StringBuilder builder(length_in_code_units());
for (size_t i = 0; i < length_in_code_units(); ++i) for (size_t i = 0; i < length_in_code_units(); ++i)
builder.append_code_unit(AK::to_ascii_lowercase(code_unit_at(i))); builder.append_code_unit(AK::to_ascii_lowercase(code_unit_at(i)));
return builder.to_utf16_string(); return builder.to_string();
} }
Utf16String Utf16View::to_ascii_uppercase() const Utf16String Utf16View::to_ascii_uppercase() const
{ {
StringBuilder builder(StringBuilder::Mode::UTF16, length_in_code_units()); Utf16StringBuilder builder(length_in_code_units());
for (size_t i = 0; i < length_in_code_units(); ++i) for (size_t i = 0; i < length_in_code_units(); ++i)
builder.append_code_unit(AK::to_ascii_uppercase(code_unit_at(i))); builder.append_code_unit(AK::to_ascii_uppercase(code_unit_at(i)));
return builder.to_utf16_string(); return builder.to_string();
} }
Utf16String Utf16View::to_ascii_titlecase() const Utf16String Utf16View::to_ascii_titlecase() const
{ {
StringBuilder builder(StringBuilder::Mode::UTF16, length_in_code_units()); Utf16StringBuilder builder(length_in_code_units());
bool next_is_upper = true; bool next_is_upper = true;
for (size_t i = 0; i < length_in_code_units(); ++i) { for (size_t i = 0; i < length_in_code_units(); ++i) {
@ -93,7 +93,7 @@ Utf16String Utf16View::to_ascii_titlecase() const
next_is_upper = code_unit == u' '; next_is_upper = code_unit == u' ';
} }
return builder.to_utf16_string(); return builder.to_string();
} }
Utf16String Utf16View::replace(char16_t needle, Utf16View const& replacement, ReplaceMode replace_mode) const Utf16String Utf16View::replace(char16_t needle, Utf16View const& replacement, ReplaceMode replace_mode) const
@ -106,7 +106,7 @@ Utf16String Utf16View::replace(Utf16View const& needle, Utf16View const& replace
if (is_empty()) if (is_empty())
return {}; return {};
StringBuilder builder(StringBuilder::Mode::UTF16, length_in_code_units()); Utf16StringBuilder builder(length_in_code_units());
auto remaining = *this; auto remaining = *this;
do { do {
@ -122,12 +122,12 @@ Utf16String Utf16View::replace(Utf16View const& needle, Utf16View const& replace
} while (replace_mode == ReplaceMode::All && !remaining.is_empty()); } while (replace_mode == ReplaceMode::All && !remaining.is_empty());
builder.append(remaining); builder.append(remaining);
return builder.to_utf16_string(); return builder.to_string();
} }
Utf16String Utf16View::escape_html_entities() const Utf16String Utf16View::escape_html_entities() const
{ {
StringBuilder builder(StringBuilder::Mode::UTF16, length_in_code_units()); Utf16StringBuilder builder(length_in_code_units());
for (auto code_point : *this) { for (auto code_point : *this) {
if (code_point == '<') if (code_point == '<')
@ -142,7 +142,7 @@ Utf16String Utf16View::escape_html_entities() const
builder.append_code_point(code_point); builder.append_code_point(code_point);
} }
return builder.to_utf16_string(); return builder.to_string();
} }
bool Utf16View::is_ascii() const bool Utf16View::is_ascii() const

View file

@ -687,7 +687,7 @@ template<>
struct Formatter<Utf16View> : Formatter<FormatString> { struct Formatter<Utf16View> : Formatter<FormatString> {
ErrorOr<void> format(FormatBuilder& builder, Utf16View const& value) ErrorOr<void> format(FormatBuilder& builder, Utf16View const& value)
{ {
return builder.builder().try_append(value); return builder.put_string(value);
} }
}; };

View file

@ -354,7 +354,7 @@ TEST_CASE(repeated)
TEST_CASE(from_string_builder) TEST_CASE(from_string_builder)
{ {
StringBuilder builder(StringBuilder::Mode::UTF16); Utf16StringBuilder builder;
builder.append_code_point('a'); builder.append_code_point('a');
builder.append_code_point('b'); builder.append_code_point('b');
builder.append_code_point(0x1f600); builder.append_code_point(0x1f600);
@ -363,7 +363,7 @@ TEST_CASE(from_string_builder)
builder.append_code_point('c'); builder.append_code_point('c');
builder.append_code_point('d'); builder.append_code_point('d');
auto string = builder.to_utf16_string(); auto string = builder.to_string();
EXPECT_EQ(string.length_in_code_units(), 10uz); EXPECT_EQ(string.length_in_code_units(), 10uz);
EXPECT_EQ(string.length_in_code_points(), 7uz); EXPECT_EQ(string.length_in_code_points(), 7uz);
EXPECT_EQ(string, "ab😀𐀀🍕cd"sv); EXPECT_EQ(string, "ab😀𐀀🍕cd"sv);
@ -371,8 +371,8 @@ TEST_CASE(from_string_builder)
TEST_CASE(from_string_builder_alignment) TEST_CASE(from_string_builder_alignment)
{ {
StringBuilder builder(StringBuilder::Mode::UTF16); Utf16StringBuilder builder;
builder.append("\u00a0"sv); builder.append(Utf16String::from_utf8_without_validation("\u00a0"sv));
builder.append(R"~~( builder.append(R"~~(
<script> <script>
const containsValidURL = input => { const containsValidURL = input => {
@ -381,7 +381,7 @@ TEST_CASE(from_string_builder_alignment)
</script> </script>
)~~"sv); )~~"sv);
auto string1 = builder.to_utf16_string(); auto string1 = builder.to_string();
auto string2 = string1.to_utf8(); auto string2 = string1.to_utf8();
EXPECT_EQ(string1.code_unit_at(0), 0x00a0); EXPECT_EQ(string1.code_unit_at(0), 0x00a0);
@ -415,10 +415,7 @@ TEST_CASE(from_ipc_stream)
{ {
auto data = u"hello 😀 there!"sv; auto data = u"hello 😀 there!"sv;
StringBuilder builder(StringBuilder::Mode::UTF16); auto buffer = MUST(ByteBuffer::copy({ reinterpret_cast<u8 const*>(data.utf16_span().data()), data.length_in_code_units() * sizeof(char16_t) }));
builder.append(data);
auto buffer = MUST(builder.to_byte_buffer());
FixedMemoryStream stream { buffer.bytes() }; FixedMemoryStream stream { buffer.bytes() };
auto string = TRY_OR_FAIL(Utf16String::from_ipc_stream(stream, data.length_in_code_units(), false)); auto string = TRY_OR_FAIL(Utf16String::from_ipc_stream(stream, data.length_in_code_units(), false));
@ -438,10 +435,7 @@ TEST_CASE(from_ipc_stream)
{ {
auto data = u"😀"sv; auto data = u"😀"sv;
StringBuilder builder(StringBuilder::Mode::UTF16); auto buffer = MUST(ByteBuffer::copy({ reinterpret_cast<u8 const*>(data.utf16_span().data()), data.length_in_code_units() * sizeof(char16_t) }));
builder.append(data);
auto buffer = MUST(builder.to_byte_buffer());
FixedMemoryStream stream { buffer.bytes() }; FixedMemoryStream stream { buffer.bytes() };
auto result = Utf16String::from_ipc_stream(stream, data.length_in_code_units(), true); auto result = Utf16String::from_ipc_stream(stream, data.length_in_code_units(), true);
@ -450,10 +444,7 @@ TEST_CASE(from_ipc_stream)
{ {
auto data = u"hello 😀 there!"sv; auto data = u"hello 😀 there!"sv;
StringBuilder builder(StringBuilder::Mode::UTF16); auto buffer = MUST(ByteBuffer::copy({ reinterpret_cast<u8 const*>(data.utf16_span().data()), data.length_in_code_units() * sizeof(char16_t) }));
builder.append(data);
auto buffer = MUST(builder.to_byte_buffer());
FixedMemoryStream stream { buffer.bytes() }; FixedMemoryStream stream { buffer.bytes() };
auto result = Utf16String::from_ipc_stream(stream, data.length_in_code_units(), true); auto result = Utf16String::from_ipc_stream(stream, data.length_in_code_units(), true);
@ -1251,17 +1242,16 @@ TEST_CASE(optional)
EXPECT_EQ(released, u"well 😀 hello"sv); EXPECT_EQ(released, u"well 😀 hello"sv);
} }
TEST_CASE(utf16_builder_clear_resets_ascii_flag) TEST_CASE(utf16_string_builder_clear_resets_ascii_flag)
{ {
// Regression test: StringBuilder(UTF16) must reset m_utf16_builder_is_ascii // Regression test: Utf16StringBuilder must reset its ASCII state on clear().
// on clear(). Without this, a builder that previously held non-ASCII content // Without this, a builder that previously held non-ASCII content stores
// stores subsequent ASCII as char16_t, and to_utf16_string() corrupts the // subsequent ASCII as char16_t, corrupting the first code unit when adopted.
// first code unit via placement-new overlap in from_string_builder(). Utf16StringBuilder builder;
StringBuilder builder(StringBuilder::Mode::UTF16);
// 1. Append a non-ASCII code point to force m_utf16_builder_is_ascii = false. // 1. Append a non-ASCII code point to force m_utf16_builder_is_ascii = false.
builder.append_code_point(0x00D7); // U+00D7 MULTIPLICATION SIGN (×) builder.append_code_point(0x00D7); // U+00D7 MULTIPLICATION SIGN (×)
auto first = builder.to_utf16_string(); auto first = builder.to_string();
EXPECT_EQ(first.length_in_code_units(), 1u); EXPECT_EQ(first.length_in_code_units(), 1u);
EXPECT_EQ(first.code_unit_at(0), 0x00D7); EXPECT_EQ(first.code_unit_at(0), 0x00D7);
@ -1271,7 +1261,7 @@ TEST_CASE(utf16_builder_clear_resets_ascii_flag)
for (int i = 0; i < 100; ++i) for (int i = 0; i < 100; ++i)
builder.append_code_point(' '); builder.append_code_point(' ');
auto second = builder.to_utf16_string(); auto second = builder.to_string();
EXPECT_EQ(second.length_in_code_units(), 101u); EXPECT_EQ(second.length_in_code_units(), 101u);
EXPECT_EQ(second.code_unit_at(0), 0x000A); // Must be newline, not null. EXPECT_EQ(second.code_unit_at(0), 0x000A); // Must be newline, not null.
EXPECT_EQ(second.code_unit_at(1), 0x0020); EXPECT_EQ(second.code_unit_at(1), 0x0020);

View file

@ -5,6 +5,7 @@
*/ */
#include <AK/StringBuilder.h> #include <AK/StringBuilder.h>
#include <AK/Utf16StringBuilder.h>
#include <LibURL/Parser.h> #include <LibURL/Parser.h>
#include <UI/Qt/StringUtils.h> #include <UI/Qt/StringUtils.h>
@ -44,9 +45,9 @@ QString qstring_from_utf16_string(Utf16View const& string)
QString vqformatted(StringView format, AK::TypeErasedFormatParams& parameters) QString vqformatted(StringView format, AK::TypeErasedFormatParams& parameters)
{ {
StringBuilder builder(StringBuilder::Mode::UTF16); Utf16StringBuilder builder;
MUST(vformat(builder, format, parameters)); MUST(vformat(builder, format, parameters));
return qstring_from_utf16_string(builder.utf16_string_view()); return qstring_from_utf16_string(builder.view());
} }
QByteArray qbytearray_from_ak_string(StringView ak_string) QByteArray qbytearray_from_ak_string(StringView ak_string)