Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Conversation

@Renzo-Olivares
Copy link
Contributor

@Renzo-Olivares Renzo-Olivares commented Jun 27, 2023

Fixes flutter/flutter#129061

Pre-launch Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I read the [Tree Hygiene] wiki page, which explains my responsibilities.
  • I read and followed the [Flutter Style Guide] and the [C++, Objective-C, Java style guides].
  • I listed at least one issue that this PR fixes in the description above.
  • I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test-exempt. See [testing the engine] for instructions on writing and running engine tests.
  • I updated/added relevant documentation (doc comments with ///).
  • I signed the [CLA].
  • All existing and new tests are passing.

@Renzo-Olivares Renzo-Olivares marked this pull request as ready for review June 30, 2023 21:16
@Renzo-Olivares
Copy link
Contributor Author

Some observations:

When typing a sequence that results in an em-dash:
- followed by another - will be transformed into . deleteBackwards is called to remove the initial - and then insertText is called to insert the em dash. Before this change the deleteBackwards and insertText would be sent as individual delta batches, i.e. updateEditingValueWithDeltas would be called twice. Sending individual batches would cause the editing state to become out of sync in some cases where the editing state is also changed on the framework side in the middle of this em dash transformation. After this change deleteBackwards and insertText are sent in one batch, i.e. a single call to updateEditingValueWithDeltas. This gives the user a chance to process an entire batch of deltas before making any edits to the editing state.

I also tried to observe what a native UITextField does in this situation by listening to the textDidChange event. I only observed one textDidChange call when doing an em-dash transformation.

[self.textField addTarget:self action:@selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged];

Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM but @LongCatIsLooong should definitely take a look.

The run loop thing looks like an elegant solution to this problem. It seems like that's how things like the em dash are supposed to be handled.

- (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)text {
// `temporarilyDeletedComposedCharacter` should only be used during a single text change session.
// So it needs to be cleared at the start of each text editting session.
// So it needs to be cleared at the start of each text editing session.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕵️

Comment on lines 1427 to 1437
done = false;
XCTAssertFalse(done);
CFRunLoopPerformBlock(CFRunLoopGetMain(), (__bridge CFStringRef)runLoopMode, ^{
done = true;
});

while (!done) {
// Each invocation will handle one source.
CFRunLoopRunInMode((__bridge CFStringRef)runLoopMode, 0, true);
}
XCTAssertTrue(done);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be any cleaner to abstract this code to a function or something? It's kind of a bummer that we have to clear the runloop all over the place like this, but it makes sense. The next time a test is added here, it should be obvious that this is necessary since it's all over.

_markedRect = kInvalidFirstRect;
_cachedFirstRect = kInvalidFirstRect;
_scribbleInteractionStatus = FlutterScribbleInteractionStatusNone;
_pendingDeltas = [[NSMutableArray alloc] init];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have to release this somewhere? I'm just guessing since I'm no objective c expert.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No release here is needed as this file is ARC.

@stuartmorgan-g stuartmorgan-g requested a review from cyanglaz July 10, 2023 19:14
@stuartmorgan-g
Copy link
Contributor

Please wait for @cyanglaz to review as well for ObjC expertise.

_markedRect = kInvalidFirstRect;
_cachedFirstRect = kInvalidFirstRect;
_scribbleInteractionStatus = FlutterScribbleInteractionStatusNone;
_pendingDeltas = [[NSMutableArray alloc] init];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No release here is needed as this file is ARC.

@property(nonatomic, copy) NSDictionary* markedTextStyle;
@property(nonatomic, weak) id<UITextInputDelegate> inputDelegate;
@property(nonatomic, strong) NSMutableArray* pendingDeltas;
@property(nonatomic, readwrite, copy) NSString* customRunLoopMode;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: readwrite is the default so it is not necessary here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you don't need to expose the run loop mode? IIRC in the macOS implementation it was only exposed because there was a leak in the tests so we couldn't use async expectations.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I think about it, do you need to use the runloop API? dispatch_async does the same thing right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I think dispatch_async should accomplish the same thing and we wouldn't need to set the mode either. I can try that.


[self.textInputDelegate flutterTextInputView:self
updateEditingClient:_textInputClient
withDelta:deltas];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is fine but we should confirm that the framework can handle any delta batches. cc @LongCatIsLooong

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you referring to the frameworks ability to receive/apply delta batches?

NSDictionary* deltas = @{
@"deltas" : @[ deltaToFramework ],
};
[_pendingDeltas addObject:deltaToFramework];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that there's batching, what happens if oldText is different in the same batch? For example when there's a framework change happened in between?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean in this situation:

Batch: Insertion, Deletion, Insertion
After first insertion delta is applied, the framework changes the editing state in response to the insertion?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the engine applies an update from the framework, after the block is scheduled and before the block is invoked. In theory that can happen right?

Copy link
Contributor Author

@Renzo-Olivares Renzo-Olivares Jul 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean. In theory that can happen something like the following example.
Type A followed by B:

Engine types A.
Engine schedules delta block to run.
Framework types C for x reason.
Engine has C.
Engine types B.
Engine has CB and sends to framework, Expected ACB?

Deltas sent:
InsertionA, oldText: ''
InsertionB, oldText:'C'.

Here the user will not really be able to recover. I don't think this is too different from the IME-Framework out of sync issues we usually face. What do you think about creating some method that signals the framework that a batch has been scheduled, and that it probably is not safe to make any framework side changes during this time.

Another thing to note is that Android already does batching of TextEditingDeltas on its TextInputPlugin, so maybe this is an issue we already face on that end.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In your example the batch is going to be partially applied by the framework right? The framework will apply changes in the batch until the point where oldText mismatches? Shouldn't it be all or nothing, when there's on inconsistent "oldText", when framework rejects the entire batch?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the framework will ignore any difference in the oldText. https://github.com/flutter/flutter/blob/f468f3366c26a5092eb964a230ce7892fda8f2f8/packages/flutter/lib/src/services/text_editing_delta.dart#L324-L334 . A TextEditingDelta applies the delta to the oldText to follow a last write wins policy. A user can still prevent the application of a delta by checking the oldText with their current value themselves before calling apply. A user can make it all or nothing but we don't do this by default.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry missed the reply.

A user can make it all or nothing but we don't do this by default

So the current implementation in the framework doesn't guarantee that a delta batch is atomically applied? That sounds like the same issue could still happen if a framework edit is sent to the engine while the engine is replacing a double-dash?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh the framework edit is going to be completely ignored in that case?

Copy link
Contributor Author

@Renzo-Olivares Renzo-Olivares Jul 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the current implementation in the framework doesn't guarantee that a delta batch is atomically applied? That sounds like the same issue could still happen if a framework edit is sent to the engine while the engine is replacing a double-dash?

Yes in theory a framework edit can still come in during the batch accumulation and interrupt the double-dash replacement. I don't think we can avoid this case but I don't think it is less likely. Before this change the double-dash replacement sequence would come in as 3 separate delta batches, so the user could potentially react to the 1st delta batch and make some edit and disrupt the sequence. After this change the sequence comes in as one delta batch, so the user can avoid making an edit until they fully apply the delta batch.

Oh the framework edit is going to be completely ignored in that case?

No, it is still applied by the engine. In the example I stated above the edit that is ignored is the 1st engine edit that types 'A'.

@property(nonatomic, copy) NSDictionary* markedTextStyle;
@property(nonatomic, weak) id<UITextInputDelegate> inputDelegate;
@property(nonatomic, strong) NSMutableArray* pendingDeltas;
@property(nonatomic, readwrite, copy) NSString* customRunLoopMode;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you don't need to expose the run loop mode? IIRC in the macOS implementation it was only exposed because there was a leak in the tests so we couldn't use async expectations.

: kCFRunLoopCommonModes;

CFRunLoopPerformBlock(CFRunLoopGetMain(), runLoopMode, ^{
if (_pendingDeltas.count > 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible for the text input plugin to already be released when the block is invoked?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, i'm not too familiar with objc memory management. I would think it is possible since the block is run asynchronously. Is this something we should handle?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use __weak maybe?

__weak FlutterTextInputView* weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
__strong FlutterTextInputView* strongSelf = weakSelf;
NSAssert(strongSelf,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is something that can happen legitimately?

}];
}

- (void)dispatchAsyncOnMain {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flushScheduledAsyncBlocks, or some such?

[inputView insertText:@""];

[self dispatchAsyncOnMain];
// __block bool done = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove commented code here?

@chinmaygarde
Copy link
Contributor

May I add the autosubmit tag?

@Renzo-Olivares Renzo-Olivares added the autosubmit Merge PR when tree becomes green via auto submit App label Jul 27, 2023
@auto-submit auto-submit bot merged commit 78171a6 into flutter:main Jul 27, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Jul 27, 2023
auto-submit bot pushed a commit to flutter/flutter that referenced this pull request Jul 27, 2023
flutter/engine@284771d...196474b

2023-07-27 skia-flutter-autoroll@skia.org Roll Skia from 20108acef7d2 to d3a7efb77af3 (2 revisions) (flutter/engine#44082)
2023-07-27 rmolivares@renzo-olivares.dev [iOS] TextInputPlugin should batch TextEditingDeltas and dispatch on the next runLoop (flutter/engine#43267)
2023-07-27 skia-flutter-autoroll@skia.org Roll Skia from b522808eb0af to 20108acef7d2 (3 revisions) (flutter/engine#44079)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-engine-flutter-autoroll
Please CC jonahwilliams@google.com,rmistry@google.com,zra@google.com on the revert to ensure that a human
is aware of the problem.

To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
LouiseHsu pushed a commit to LouiseHsu/flutter that referenced this pull request Jul 31, 2023
…1443)

flutter/engine@284771d...196474b

2023-07-27 skia-flutter-autoroll@skia.org Roll Skia from 20108acef7d2 to d3a7efb77af3 (2 revisions) (flutter/engine#44082)
2023-07-27 rmolivares@renzo-olivares.dev [iOS] TextInputPlugin should batch TextEditingDeltas and dispatch on the next runLoop (flutter/engine#43267)
2023-07-27 skia-flutter-autoroll@skia.org Roll Skia from b522808eb0af to 20108acef7d2 (3 revisions) (flutter/engine#44079)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-engine-flutter-autoroll
Please CC jonahwilliams@google.com,rmistry@google.com,zra@google.com on the revert to ensure that a human
is aware of the problem.

To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
vashworth pushed a commit to vashworth/flutter that referenced this pull request Aug 2, 2023
…1443)

flutter/engine@284771d...196474b

2023-07-27 skia-flutter-autoroll@skia.org Roll Skia from 20108acef7d2 to d3a7efb77af3 (2 revisions) (flutter/engine#44082)
2023-07-27 rmolivares@renzo-olivares.dev [iOS] TextInputPlugin should batch TextEditingDeltas and dispatch on the next runLoop (flutter/engine#43267)
2023-07-27 skia-flutter-autoroll@skia.org Roll Skia from b522808eb0af to 20108acef7d2 (3 revisions) (flutter/engine#44079)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-engine-flutter-autoroll
Please CC jonahwilliams@google.com,rmistry@google.com,zra@google.com on the revert to ensure that a human
is aware of the problem.

To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
gaaclarke pushed a commit to gaaclarke/engine that referenced this pull request Aug 30, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

autosubmit Merge PR when tree becomes green via auto submit App platform-ios

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[iOS] Typing two dashes generate unexpected deltas

6 participants