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

Fix loss of negative text selection ranges #19785

Merged
merged 5 commits into from
Jul 17, 2020

Conversation

justinmc
Copy link
Contributor

Description

In the framework, changing the value of a TextEditingController to something with a selection of (-1, -1) was coming back from the engine as (length, length). This was because of improper handling of signed and unsigned integers. The -1 was being interpreted as a very large unsigned integer, which was then clamped to the length of the string.

This PR fixes the problem by reading negative selection range values as zero.

Related Issues

Related to flutter/flutter#61282

Tests

I added the following tests:

Replace this with a list of the tests that you added as part of this PR. A
change in behaviour with no test covering it will likely get reverted
accidentally sooner or later. PRs must include tests for all
changed/updated/fixed behaviors. See [testing the engine] for instructions on
writing and running engine tests.

Breaking Change

This is unlikely to break anything unless there is an iOS-only app (Android behaves correctly) that relies on this broken behavior.

// NSRange locations, which are unsigned integers, but are read in from signed
// integers in state. These are -1 when there is no selection specified, but
// they should be interpreted as 0 instead of overflowing.
static NSUInteger readInt(NSInteger value) {
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 we can just use the MAX macro instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

MIN and MAX appear to cast their inputs to unsigned integers, which is the cause of this bug. I was seeing things like MAX(-1, 0) => 2,147,483,647, and MIN(-1, 0) => 0. This function lets us be sure we're sending sensible unsigned ints to MIN and MAX.

Copy link
Contributor

@LongCatIsLooong LongCatIsLooong Jul 16, 2020

Choose a reason for hiding this comment

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

Hmm maybe that's because -1 and 0 are number literals, so their type was inferred to NSUInteger before the comparison took place? I think MAX should work fine if you're converting the number from an NSNumber using intValue.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here's a specific example I was able to get while running the app. I saw it at this line:

int start = MIN(MAX(range.location, 0), text.length);

I'm logging like this:

NSLog(@"MAX(%li, 0) => %li, MIN(MAX(range.location, 0), %li) => %li", range.location, MAX(range.location, 0), text.length, MIN(MAX(range.location, 0), text.length));

When range.location is -1 and text.length is 2 this is what I see logged:

MAX(-1, 0) => -1, MIN(MAX(range.location, 0), 2) => 2

But I would expect the MAX to return 0 and the overall MIN to return 0 as well.

@@ -347,8 +355,8 @@ - (BOOL)setTextInputState:(NSDictionary*)state {
: [newMarkedRange isEqualTo:(FlutterTextRange*)self.markedTextRange];
self.markedTextRange = newMarkedRange;

NSInteger selectionBase = [state[@"selectionBase"] intValue];
NSInteger selectionExtent = [state[@"selectionExtent"] intValue];
NSUInteger selectionBase = readInt([state[@"selectionBase"] intValue]);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure what an empty selection stands for. Shouldn't there always be a selected range?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That confused me too. I'm not sure if there is a use case for that, but it seems to often default to (-1, -1) in the framework. In the case of this bug, for example, the user was setting the text of a TextEditingController, which explicitly sets the selection to (-1, -1) (code). The cursor ends up at the beginning of the field.

Copy link
Contributor

Choose a reason for hiding this comment

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

On android it translates to Selection.removeSelection(mEditable);. Not sure the same option exists on iOS.

private void applyStateToSelection(TextInputChannel.TextEditState state) {
int selStart = state.selectionStart;
int selEnd = state.selectionEnd;
if (selStart >= 0
&& selStart <= mEditable.length()
&& selEnd >= 0
&& selEnd <= mEditable.length()) {
Selection.setSelection(mEditable, selStart, selEnd);
} else {
Selection.removeSelection(mEditable);
}
}

@justinmc justinmc force-pushed the ios-range-overflow branch 2 times, most recently from f5856c3 to 7abfce1 Compare July 16, 2020 20:54
@justinmc justinmc force-pushed the ios-range-overflow branch from 7abfce1 to 6bf1de7 Compare July 16, 2020 20:55
@justinmc
Copy link
Contributor Author

@LongCatIsLooong FYI updated and commented as we discussed, in case you want to take another look before merge.

@LongCatIsLooong
Copy link
Contributor

LGTM 👍

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants