[Linux] composing delta fixes (flutter/engine#42648)
Fixes a few issues with the Linux text editing delta implementation that was causing some issues when composing CJK text. `im_preedit_changed_cb` dispatches a delta with the delta range set as the current composing region causing a crash when composing because the new composing region range might not yet exist in the `oldText` that the delta is applied to. We should instead send the composing region before the change to the text as that will be the correct range that is being modified in the `oldText`. `im_preedit_end_cb` dispatches a delta with an empty `oldText`, a not empty `deltaText`, and a delta range of `(-1,-1)` causing a crash when composing ends because the delta range will never be within the range of the text. We should instead send the current text value as the `oldText` and not send a `deltaRange` or `deltaText` since a `(-1,-1)` range essentially means nothing in the text changed. `im_commit_cb` does not account for the case when we were previously composing when the text was committed. This causes the dispatched delta to have a delta range that always inserts at the current the selection, when it should instead replace the previous composing range. We should account for the case when we are previously composing before committing the text, and dispatch a delta with the previous composing region. Fixes https://github.com/flutter/flutter/issues/113909
This commit is contained in:
@@ -263,6 +263,8 @@ static void im_preedit_changed_cb(FlTextInputPlugin* self) {
|
||||
FlTextInputPluginPrivate* priv = static_cast<FlTextInputPluginPrivate*>(
|
||||
fl_text_input_plugin_get_instance_private(self));
|
||||
std::string text_before_change = priv->text_model->GetText();
|
||||
flutter::TextRange composing_before_change =
|
||||
priv->text_model->composing_range();
|
||||
g_autofree gchar* buf = nullptr;
|
||||
gint cursor_offset = 0;
|
||||
gtk_im_context_get_preedit_string(priv->im_context, &buf, nullptr,
|
||||
@@ -278,7 +280,7 @@ static void im_preedit_changed_cb(FlTextInputPlugin* self) {
|
||||
if (priv->enable_delta_model) {
|
||||
std::string text(buf);
|
||||
flutter::TextEditingDelta delta = flutter::TextEditingDelta(
|
||||
text_before_change, priv->text_model->composing_range(), text);
|
||||
text_before_change, composing_before_change, text);
|
||||
update_editing_state_with_delta(self, &delta);
|
||||
} else {
|
||||
update_editing_state(self);
|
||||
@@ -290,7 +292,10 @@ static void im_commit_cb(FlTextInputPlugin* self, const gchar* text) {
|
||||
FlTextInputPluginPrivate* priv = static_cast<FlTextInputPluginPrivate*>(
|
||||
fl_text_input_plugin_get_instance_private(self));
|
||||
std::string text_before_change = priv->text_model->GetText();
|
||||
flutter::TextRange composing_before_change =
|
||||
priv->text_model->composing_range();
|
||||
flutter::TextRange selection_before_change = priv->text_model->selection();
|
||||
gboolean was_composing = priv->text_model->composing();
|
||||
|
||||
priv->text_model->AddText(text);
|
||||
if (priv->text_model->composing()) {
|
||||
@@ -298,9 +303,12 @@ static void im_commit_cb(FlTextInputPlugin* self, const gchar* text) {
|
||||
}
|
||||
|
||||
if (priv->enable_delta_model) {
|
||||
flutter::TextEditingDelta delta = flutter::TextEditingDelta(
|
||||
text_before_change, selection_before_change, text);
|
||||
update_editing_state_with_delta(self, &delta);
|
||||
flutter::TextRange replace_range =
|
||||
was_composing ? composing_before_change : selection_before_change;
|
||||
std::unique_ptr<flutter::TextEditingDelta> delta =
|
||||
std::make_unique<flutter::TextEditingDelta>(text_before_change,
|
||||
replace_range, text);
|
||||
update_editing_state_with_delta(self, delta.get());
|
||||
} else {
|
||||
update_editing_state(self);
|
||||
}
|
||||
@@ -312,8 +320,8 @@ static void im_preedit_end_cb(FlTextInputPlugin* self) {
|
||||
fl_text_input_plugin_get_instance_private(self));
|
||||
priv->text_model->EndComposing();
|
||||
if (priv->enable_delta_model) {
|
||||
flutter::TextEditingDelta delta = flutter::TextEditingDelta(
|
||||
"", flutter::TextRange(-1, -1), priv->text_model->GetText());
|
||||
flutter::TextEditingDelta delta =
|
||||
flutter::TextEditingDelta(priv->text_model->GetText());
|
||||
update_editing_state_with_delta(self, &delta);
|
||||
} else {
|
||||
update_editing_state(self);
|
||||
|
||||
@@ -958,6 +958,8 @@ TEST(FlTextInputPluginTest, ComposingDelta) {
|
||||
|
||||
messenger.ReceiveMessage("flutter/textinput", set_client);
|
||||
|
||||
g_signal_emit_by_name(context, "preedit-start", nullptr);
|
||||
|
||||
// update
|
||||
EXPECT_CALL(context,
|
||||
gtk_im_context_get_preedit_string(
|
||||
@@ -976,7 +978,7 @@ TEST(FlTextInputPluginTest, ComposingDelta) {
|
||||
.old_text = "",
|
||||
.delta_text = "Flutter ",
|
||||
.delta_start = 0,
|
||||
.delta_end = 8,
|
||||
.delta_end = 0,
|
||||
.selection_base = 8,
|
||||
.selection_extent = 8,
|
||||
.composing_base = 0,
|
||||
@@ -1004,13 +1006,13 @@ TEST(FlTextInputPluginTest, ComposingDelta) {
|
||||
build_list({
|
||||
build_editing_delta({
|
||||
.old_text = "Flutter ",
|
||||
.delta_text = "engine",
|
||||
.delta_start = 8,
|
||||
.delta_text = "Flutter engine",
|
||||
.delta_start = 0,
|
||||
.delta_end = 8,
|
||||
.selection_base = 14,
|
||||
.selection_extent = 14,
|
||||
.composing_base = 0,
|
||||
.composing_extent = 8,
|
||||
.composing_base = -1,
|
||||
.composing_extent = -1,
|
||||
}),
|
||||
}),
|
||||
}}),
|
||||
@@ -1024,7 +1026,7 @@ TEST(FlTextInputPluginTest, ComposingDelta) {
|
||||
FlValueEq(commit)),
|
||||
::testing::_, ::testing::_, ::testing::_));
|
||||
|
||||
g_signal_emit_by_name(context, "commit", "engine", nullptr);
|
||||
g_signal_emit_by_name(context, "commit", "Flutter engine", nullptr);
|
||||
|
||||
// end
|
||||
g_autoptr(FlValue) end = build_list({
|
||||
@@ -1033,7 +1035,7 @@ TEST(FlTextInputPluginTest, ComposingDelta) {
|
||||
"deltas",
|
||||
build_list({
|
||||
build_editing_delta({
|
||||
.delta_text = "Flutter engine",
|
||||
.old_text = "Flutter engine",
|
||||
.selection_base = 14,
|
||||
.selection_extent = 14,
|
||||
}),
|
||||
@@ -1051,3 +1053,241 @@ TEST(FlTextInputPluginTest, ComposingDelta) {
|
||||
|
||||
g_signal_emit_by_name(context, "preedit-end", nullptr);
|
||||
}
|
||||
|
||||
TEST(FlTextInputPluginTest, NonComposingDelta) {
|
||||
::testing::NiceMock<flutter::testing::MockBinaryMessenger> messenger;
|
||||
::testing::NiceMock<flutter::testing::MockIMContext> context;
|
||||
::testing::NiceMock<flutter::testing::MockTextInputViewDelegate> delegate;
|
||||
|
||||
g_autoptr(FlTextInputPlugin) plugin =
|
||||
fl_text_input_plugin_new(messenger, context, delegate);
|
||||
EXPECT_NE(plugin, nullptr);
|
||||
|
||||
// set config
|
||||
g_autoptr(FlValue) args = build_input_config({
|
||||
.client_id = 1,
|
||||
.enable_delta_model = true,
|
||||
});
|
||||
g_autoptr(FlJsonMethodCodec) codec = fl_json_method_codec_new();
|
||||
g_autoptr(GBytes) set_client = fl_method_codec_encode_method_call(
|
||||
FL_METHOD_CODEC(codec), "TextInput.setClient", args, nullptr);
|
||||
|
||||
g_autoptr(FlValue) null = fl_value_new_null();
|
||||
EXPECT_CALL(messenger, fl_binary_messenger_send_response(
|
||||
::testing::Eq<FlBinaryMessenger*>(messenger),
|
||||
::testing::A<FlBinaryMessengerResponseHandle*>(),
|
||||
SuccessResponse(null), ::testing::A<GError**>()))
|
||||
.WillOnce(::testing::Return(true));
|
||||
|
||||
messenger.ReceiveMessage("flutter/textinput", set_client);
|
||||
|
||||
// commit F
|
||||
g_autoptr(FlValue) commit = build_list({
|
||||
fl_value_new_int(1), // client_id
|
||||
build_map({{
|
||||
"deltas",
|
||||
build_list({
|
||||
build_editing_delta({
|
||||
.old_text = "",
|
||||
.delta_text = "F",
|
||||
.delta_start = 0,
|
||||
.delta_end = 0,
|
||||
.selection_base = 1,
|
||||
.selection_extent = 1,
|
||||
.composing_base = -1,
|
||||
.composing_extent = -1,
|
||||
}),
|
||||
}),
|
||||
}}),
|
||||
});
|
||||
|
||||
EXPECT_CALL(messenger,
|
||||
fl_binary_messenger_send_on_channel(
|
||||
::testing::Eq<FlBinaryMessenger*>(messenger),
|
||||
::testing::StrEq("flutter/textinput"),
|
||||
MethodCall("TextInputClient.updateEditingStateWithDeltas",
|
||||
FlValueEq(commit)),
|
||||
::testing::_, ::testing::_, ::testing::_));
|
||||
|
||||
g_signal_emit_by_name(context, "commit", "F", nullptr);
|
||||
|
||||
// commit l
|
||||
g_autoptr(FlValue) commitL = build_list({
|
||||
fl_value_new_int(1), // client_id
|
||||
build_map({{
|
||||
"deltas",
|
||||
build_list({
|
||||
build_editing_delta({
|
||||
.old_text = "F",
|
||||
.delta_text = "l",
|
||||
.delta_start = 1,
|
||||
.delta_end = 1,
|
||||
.selection_base = 2,
|
||||
.selection_extent = 2,
|
||||
.composing_base = -1,
|
||||
.composing_extent = -1,
|
||||
}),
|
||||
}),
|
||||
}}),
|
||||
});
|
||||
|
||||
EXPECT_CALL(messenger,
|
||||
fl_binary_messenger_send_on_channel(
|
||||
::testing::Eq<FlBinaryMessenger*>(messenger),
|
||||
::testing::StrEq("flutter/textinput"),
|
||||
MethodCall("TextInputClient.updateEditingStateWithDeltas",
|
||||
FlValueEq(commitL)),
|
||||
::testing::_, ::testing::_, ::testing::_));
|
||||
|
||||
g_signal_emit_by_name(context, "commit", "l", nullptr);
|
||||
|
||||
// commit u
|
||||
g_autoptr(FlValue) commitU = build_list({
|
||||
fl_value_new_int(1), // client_id
|
||||
build_map({{
|
||||
"deltas",
|
||||
build_list({
|
||||
build_editing_delta({
|
||||
.old_text = "Fl",
|
||||
.delta_text = "u",
|
||||
.delta_start = 2,
|
||||
.delta_end = 2,
|
||||
.selection_base = 3,
|
||||
.selection_extent = 3,
|
||||
.composing_base = -1,
|
||||
.composing_extent = -1,
|
||||
}),
|
||||
}),
|
||||
}}),
|
||||
});
|
||||
|
||||
EXPECT_CALL(messenger,
|
||||
fl_binary_messenger_send_on_channel(
|
||||
::testing::Eq<FlBinaryMessenger*>(messenger),
|
||||
::testing::StrEq("flutter/textinput"),
|
||||
MethodCall("TextInputClient.updateEditingStateWithDeltas",
|
||||
FlValueEq(commitU)),
|
||||
::testing::_, ::testing::_, ::testing::_));
|
||||
|
||||
g_signal_emit_by_name(context, "commit", "u", nullptr);
|
||||
|
||||
// commit t
|
||||
g_autoptr(FlValue) commitTa = build_list({
|
||||
fl_value_new_int(1), // client_id
|
||||
build_map({{
|
||||
"deltas",
|
||||
build_list({
|
||||
build_editing_delta({
|
||||
.old_text = "Flu",
|
||||
.delta_text = "t",
|
||||
.delta_start = 3,
|
||||
.delta_end = 3,
|
||||
.selection_base = 4,
|
||||
.selection_extent = 4,
|
||||
.composing_base = -1,
|
||||
.composing_extent = -1,
|
||||
}),
|
||||
}),
|
||||
}}),
|
||||
});
|
||||
|
||||
EXPECT_CALL(messenger,
|
||||
fl_binary_messenger_send_on_channel(
|
||||
::testing::Eq<FlBinaryMessenger*>(messenger),
|
||||
::testing::StrEq("flutter/textinput"),
|
||||
MethodCall("TextInputClient.updateEditingStateWithDeltas",
|
||||
FlValueEq(commitTa)),
|
||||
::testing::_, ::testing::_, ::testing::_));
|
||||
|
||||
g_signal_emit_by_name(context, "commit", "t", nullptr);
|
||||
|
||||
// commit t again
|
||||
g_autoptr(FlValue) commitTb = build_list({
|
||||
fl_value_new_int(1), // client_id
|
||||
build_map({{
|
||||
"deltas",
|
||||
build_list({
|
||||
build_editing_delta({
|
||||
.old_text = "Flut",
|
||||
.delta_text = "t",
|
||||
.delta_start = 4,
|
||||
.delta_end = 4,
|
||||
.selection_base = 5,
|
||||
.selection_extent = 5,
|
||||
.composing_base = -1,
|
||||
.composing_extent = -1,
|
||||
}),
|
||||
}),
|
||||
}}),
|
||||
});
|
||||
|
||||
EXPECT_CALL(messenger,
|
||||
fl_binary_messenger_send_on_channel(
|
||||
::testing::Eq<FlBinaryMessenger*>(messenger),
|
||||
::testing::StrEq("flutter/textinput"),
|
||||
MethodCall("TextInputClient.updateEditingStateWithDeltas",
|
||||
FlValueEq(commitTb)),
|
||||
::testing::_, ::testing::_, ::testing::_));
|
||||
|
||||
g_signal_emit_by_name(context, "commit", "t", nullptr);
|
||||
|
||||
// commit e
|
||||
g_autoptr(FlValue) commitE = build_list({
|
||||
fl_value_new_int(1), // client_id
|
||||
build_map({{
|
||||
"deltas",
|
||||
build_list({
|
||||
build_editing_delta({
|
||||
.old_text = "Flutt",
|
||||
.delta_text = "e",
|
||||
.delta_start = 5,
|
||||
.delta_end = 5,
|
||||
.selection_base = 6,
|
||||
.selection_extent = 6,
|
||||
.composing_base = -1,
|
||||
.composing_extent = -1,
|
||||
}),
|
||||
}),
|
||||
}}),
|
||||
});
|
||||
|
||||
EXPECT_CALL(messenger,
|
||||
fl_binary_messenger_send_on_channel(
|
||||
::testing::Eq<FlBinaryMessenger*>(messenger),
|
||||
::testing::StrEq("flutter/textinput"),
|
||||
MethodCall("TextInputClient.updateEditingStateWithDeltas",
|
||||
FlValueEq(commitE)),
|
||||
::testing::_, ::testing::_, ::testing::_));
|
||||
|
||||
g_signal_emit_by_name(context, "commit", "e", nullptr);
|
||||
|
||||
// commit r
|
||||
g_autoptr(FlValue) commitR = build_list({
|
||||
fl_value_new_int(1), // client_id
|
||||
build_map({{
|
||||
"deltas",
|
||||
build_list({
|
||||
build_editing_delta({
|
||||
.old_text = "Flutte",
|
||||
.delta_text = "r",
|
||||
.delta_start = 6,
|
||||
.delta_end = 6,
|
||||
.selection_base = 7,
|
||||
.selection_extent = 7,
|
||||
.composing_base = -1,
|
||||
.composing_extent = -1,
|
||||
}),
|
||||
}),
|
||||
}}),
|
||||
});
|
||||
|
||||
EXPECT_CALL(messenger,
|
||||
fl_binary_messenger_send_on_channel(
|
||||
::testing::Eq<FlBinaryMessenger*>(messenger),
|
||||
::testing::StrEq("flutter/textinput"),
|
||||
MethodCall("TextInputClient.updateEditingStateWithDeltas",
|
||||
FlValueEq(commitR)),
|
||||
::testing::_, ::testing::_, ::testing::_));
|
||||
|
||||
g_signal_emit_by_name(context, "commit", "r", nullptr);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user