diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 92820541de369..44349528339a3 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -570,19 +570,22 @@ - (void)insertText:(id)string replacementRange:(NSRange)range { } flutter::TextRange oldSelection = _activeModel->selection(); + flutter::TextRange composingBeforeChange = _activeModel->composing_range(); + flutter::TextRange replacedRange(-1, -1); std::string textBeforeChange = _activeModel->GetText().c_str(); std::string utf8String = [string UTF8String]; _activeModel->AddText(utf8String); if (_activeModel->composing()) { + replacedRange = composingBeforeChange; _activeModel->CommitComposing(); _activeModel->EndComposing(); + } else { + replacedRange = range.location == NSNotFound + ? flutter::TextRange(oldSelection.base(), oldSelection.extent()) + : flutter::TextRange(range.location, range.location + range.length); } if (_enableDeltaModel) { - flutter::TextRange replacedRange = - range.location == NSNotFound - ? flutter::TextRange(oldSelection.base(), oldSelection.extent()) - : flutter::TextRange(range.location, range.location + range.length); [self updateEditStateWithDelta:flutter::TextEditingDelta(textBeforeChange, replacedRange, utf8String)]; } else { @@ -625,6 +628,7 @@ - (void)setMarkedText:(id)string if (!_activeModel->composing()) { _activeModel->BeginComposing(); } + flutter::TextRange composingBeforeChange = _activeModel->composing_range(); // Input string may be NSString or NSAttributedString. BOOL isAttributedString = [string isKindOfClass:[NSAttributedString class]]; @@ -641,9 +645,8 @@ - (void)setMarkedText:(id)string _activeModel->SetSelection(flutter::TextRange(base, extent)); if (_enableDeltaModel) { - flutter::TextRange composing = _activeModel->composing_range(); - [self updateEditStateWithDelta:flutter::TextEditingDelta(textBeforeChange, composing, - marked_text)]; + [self updateEditStateWithDelta:flutter::TextEditingDelta(textBeforeChange, + composingBeforeChange, marked_text)]; } else { [self updateEditState]; } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm index 8bc3588d41cbc..17c50b26c5987 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm @@ -552,7 +552,7 @@ - (bool)testOperationsThatTriggerDelta { @"oldText" : @"text to insert", @"deltaText" : @"marked text", @"deltaStart" : @(14), - @"deltaEnd" : @(25), + @"deltaEnd" : @(14), @"selectionBase" : @(25), @"selectionExtent" : @(25), @"selectionAffinity" : @"TextAffinity.upstream", @@ -608,6 +608,243 @@ - (bool)testOperationsThatTriggerDelta { return true; } +- (bool)testComposingWithDelta { + id engineMock = OCMClassMock([FlutterEngine class]); + id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + OCMStub( // NOLINT(google-objc-avoid-throwing-exception) + [engineMock binaryMessenger]) + .andReturn(binaryMessengerMock); + + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock + nibName:@"" + bundle:nil]; + + FlutterTextInputPlugin* plugin = + [[FlutterTextInputPlugin alloc] initWithViewController:viewController]; + + [plugin handleMethodCall:[FlutterMethodCall + methodCallWithMethodName:@"TextInput.setClient" + arguments:@[ + @(1), @{ + @"inputAction" : @"action", + @"enableDeltaModel" : @"true", + @"inputType" : @{@"name" : @"inputName"}, + } + ]] + result:^(id){ + }]; + [plugin setMarkedText:@"m" selectedRange:NSMakeRange(0, 1)]; + + NSDictionary* deltaToFramework = @{ + @"oldText" : @"", + @"deltaText" : @"m", + @"deltaStart" : @(0), + @"deltaEnd" : @(0), + @"selectionBase" : @(1), + @"selectionExtent" : @(1), + @"selectionAffinity" : @"TextAffinity.upstream", + @"selectionIsDirectional" : @(false), + @"composingBase" : @(0), + @"composingExtent" : @(1), + }; + NSDictionary* expectedState = @{ + @"deltas" : @[ deltaToFramework ], + }; + + NSData* updateCall = [[FlutterJSONMethodCodec sharedInstance] + encodeMethodCall:[FlutterMethodCall + methodCallWithMethodName:@"TextInputClient.updateEditingStateWithDeltas" + arguments:@[ @(1), expectedState ]]]; + + @try { + OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) + [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]); + } @catch (...) { + return false; + } + + [plugin setMarkedText:@"ma" selectedRange:NSMakeRange(0, 1)]; + + deltaToFramework = @{ + @"oldText" : @"m", + @"deltaText" : @"ma", + @"deltaStart" : @(0), + @"deltaEnd" : @(1), + @"selectionBase" : @(2), + @"selectionExtent" : @(2), + @"selectionAffinity" : @"TextAffinity.upstream", + @"selectionIsDirectional" : @(false), + @"composingBase" : @(0), + @"composingExtent" : @(2), + }; + expectedState = @{ + @"deltas" : @[ deltaToFramework ], + }; + + updateCall = [[FlutterJSONMethodCodec sharedInstance] + encodeMethodCall:[FlutterMethodCall + methodCallWithMethodName:@"TextInputClient.updateEditingStateWithDeltas" + arguments:@[ @(1), expectedState ]]]; + + @try { + OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) + [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]); + } @catch (...) { + return false; + } + + [plugin setMarkedText:@"mar" selectedRange:NSMakeRange(0, 1)]; + + deltaToFramework = @{ + @"oldText" : @"ma", + @"deltaText" : @"mar", + @"deltaStart" : @(0), + @"deltaEnd" : @(2), + @"selectionBase" : @(3), + @"selectionExtent" : @(3), + @"selectionAffinity" : @"TextAffinity.upstream", + @"selectionIsDirectional" : @(false), + @"composingBase" : @(0), + @"composingExtent" : @(3), + }; + expectedState = @{ + @"deltas" : @[ deltaToFramework ], + }; + + updateCall = [[FlutterJSONMethodCodec sharedInstance] + encodeMethodCall:[FlutterMethodCall + methodCallWithMethodName:@"TextInputClient.updateEditingStateWithDeltas" + arguments:@[ @(1), expectedState ]]]; + + @try { + OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) + [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]); + } @catch (...) { + return false; + } + + [plugin setMarkedText:@"mark" selectedRange:NSMakeRange(0, 1)]; + + deltaToFramework = @{ + @"oldText" : @"mar", + @"deltaText" : @"mark", + @"deltaStart" : @(0), + @"deltaEnd" : @(3), + @"selectionBase" : @(4), + @"selectionExtent" : @(4), + @"selectionAffinity" : @"TextAffinity.upstream", + @"selectionIsDirectional" : @(false), + @"composingBase" : @(0), + @"composingExtent" : @(4), + }; + expectedState = @{ + @"deltas" : @[ deltaToFramework ], + }; + + updateCall = [[FlutterJSONMethodCodec sharedInstance] + encodeMethodCall:[FlutterMethodCall + methodCallWithMethodName:@"TextInputClient.updateEditingStateWithDeltas" + arguments:@[ @(1), expectedState ]]]; + + @try { + OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) + [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]); + } @catch (...) { + return false; + } + + [plugin setMarkedText:@"marke" selectedRange:NSMakeRange(0, 1)]; + + deltaToFramework = @{ + @"oldText" : @"mark", + @"deltaText" : @"marke", + @"deltaStart" : @(0), + @"deltaEnd" : @(4), + @"selectionBase" : @(5), + @"selectionExtent" : @(5), + @"selectionAffinity" : @"TextAffinity.upstream", + @"selectionIsDirectional" : @(false), + @"composingBase" : @(0), + @"composingExtent" : @(5), + }; + expectedState = @{ + @"deltas" : @[ deltaToFramework ], + }; + + updateCall = [[FlutterJSONMethodCodec sharedInstance] + encodeMethodCall:[FlutterMethodCall + methodCallWithMethodName:@"TextInputClient.updateEditingStateWithDeltas" + arguments:@[ @(1), expectedState ]]]; + + @try { + OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) + [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]); + } @catch (...) { + return false; + } + + [plugin setMarkedText:@"marked" selectedRange:NSMakeRange(0, 1)]; + + deltaToFramework = @{ + @"oldText" : @"marke", + @"deltaText" : @"marked", + @"deltaStart" : @(0), + @"deltaEnd" : @(5), + @"selectionBase" : @(6), + @"selectionExtent" : @(6), + @"selectionAffinity" : @"TextAffinity.upstream", + @"selectionIsDirectional" : @(false), + @"composingBase" : @(0), + @"composingExtent" : @(6), + }; + expectedState = @{ + @"deltas" : @[ deltaToFramework ], + }; + + updateCall = [[FlutterJSONMethodCodec sharedInstance] + encodeMethodCall:[FlutterMethodCall + methodCallWithMethodName:@"TextInputClient.updateEditingStateWithDeltas" + arguments:@[ @(1), expectedState ]]]; + + @try { + OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) + [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]); + } @catch (...) { + return false; + } + + [plugin unmarkText]; + + deltaToFramework = @{ + @"oldText" : @"marked", + @"deltaText" : @"", + @"deltaStart" : @(-1), + @"deltaEnd" : @(-1), + @"selectionBase" : @(6), + @"selectionExtent" : @(6), + @"selectionAffinity" : @"TextAffinity.upstream", + @"selectionIsDirectional" : @(false), + @"composingBase" : @(-1), + @"composingExtent" : @(-1), + }; + expectedState = @{ + @"deltas" : @[ deltaToFramework ], + }; + + updateCall = [[FlutterJSONMethodCodec sharedInstance] + encodeMethodCall:[FlutterMethodCall + methodCallWithMethodName:@"TextInputClient.updateEditingStateWithDeltas" + arguments:@[ @(1), expectedState ]]]; + + @try { + OCMVerify( // NOLINT(google-objc-avoid-throwing-exception) + [binaryMessengerMock sendOnChannel:@"flutter/textinput" message:updateCall]); + } @catch (...) { + return false; + } + return true; +} + - (bool)testLocalTextAndSelectionUpdateAfterDelta { id engineMock = OCMClassMock([FlutterEngine class]); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); @@ -716,6 +953,10 @@ - (bool)testLocalTextAndSelectionUpdateAfterDelta { ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testOperationsThatTriggerDelta]); } +TEST(FlutterTextInputPluginTest, TestComposingWithDelta) { + ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testComposingWithDelta]); +} + TEST(FlutterTextInputPluginTest, TestLocalTextAndSelectionUpdateAfterDelta) { ASSERT_TRUE([[FlutterInputPluginTestObjc alloc] testLocalTextAndSelectionUpdateAfterDelta]); }