diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 5cd463e8f83..969a4db69f9 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -762,6 +762,18 @@ static char markerKey; size_t extent = std::clamp(location + signedLength, 0L, textLength); _activeModel->SetSelection(flutter::TextRange(base, extent)); + } else if (_activeModel->composing() && + !(_activeModel->composing_range() == _activeModel->selection())) { + // When confirmed by Japanese IME, string replaces range of composing_range. + // If selection == composing_range there is no problem. + // If selection ! = composing_range the range of selection is only a part of composing_range. + // Since _activeModel->AddText is processed first for selection, the finalization of the + // conversion cannot be processed correctly unless selection == composing_range or + // selection.collapsed(). Since _activeModel->SetSelection fails if (composing_ && + // !range.collapsed()), selection == composing_range will failed. Therefore, the selection + // cursor should only be placed at the beginning of composing_range. + flutter::TextRange composing_range = _activeModel->composing_range(); + _activeModel->SetSelection(flutter::TextRange(composing_range.start())); } flutter::TextRange oldSelection = _activeModel->selection(); diff --git a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm index 0377f520469..326960d82cb 100644 --- a/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm +++ b/engine/src/flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm @@ -2027,6 +2027,51 @@ static const FlutterViewIdentifier kViewId = 1; return true; } +- (bool)testInsertTextWithCollapsedSelectionInsideComposing { + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); + id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + OCMStub([engineMock binaryMessenger]).andReturn(binaryMessengerMock); + + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock + nibName:@"" + bundle:nil]; + FlutterTextInputPluginTestDelegate* delegate = + [[FlutterTextInputPluginTestDelegate alloc] initWithBinaryMessenger:binaryMessengerMock + viewController:viewController]; + FlutterTextInputPlugin* plugin = [[FlutterTextInputPlugin alloc] initWithDelegate:delegate]; + + NSDictionary* setClientConfig = @{ + @"viewId" : @(kViewId), + @"inputAction" : @"action", + @"inputType" : @{@"name" : @"text"}, + }; + [plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient" + arguments:@[ @(1), setClientConfig ]] + result:^(id result){ + }]; + + FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditingState" + arguments:@{ + @"text" : @"今日は家に帰ります", + @"selectionBase" : @(0), + @"selectionExtent" : @(3), + @"composingBase" : @(0), + @"composingExtent" : @(9), + }]; + [plugin handleMethodCall:call + result:^(id result){ + }]; + + [plugin insertText:@"今日は家に帰ります" replacementRange:NSMakeRange(NSNotFound, 0)]; + + NSDictionary* editingState = [plugin editingState]; + EXPECT_STREQ([editingState[@"text"] UTF8String], "今日は家に帰ります"); + EXPECT_EQ([editingState[@"selectionBase"] intValue], 9); + EXPECT_EQ([editingState[@"selectionExtent"] intValue], 9); + + return true; +} + @end namespace flutter::testing { @@ -2435,4 +2480,9 @@ TEST(FlutterTextInputPluginTest, WorksWithoutViewId) { ASSERT_TRUE(plugin.currentViewController == viewController); } +TEST(FlutterTextInputPluginTest, InsertTextWithCollapsedSelectionInsideComposing) { + ASSERT_TRUE( + [[FlutterInputPluginTestObjc alloc] testInsertTextWithCollapsedSelectionInsideComposing]); +} + } // namespace flutter::testing