-
Notifications
You must be signed in to change notification settings - Fork 6k
iOS spell-checker ObjC #32941
iOS spell-checker ObjC #32941
Conversation
| _binaryMessenger.parent = nil; | ||
| [_binaryMessenger release]; | ||
| [_isolateId release]; | ||
| [self.flutterSpellCheckPlugin release]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't reference self in dealloc.
| #import <UIKit/UIApplication.h> | ||
| #import <UIKit/UIKit.h> | ||
|
|
||
| #include "flutter/fml/logging.h" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any idea why we ever #include instead of #import in the darwin shell?
| - (instancetype)initWithMethodChannel:(FlutterMethodChannel*)methodChannel { | ||
| self = [super init]; | ||
| if (self) { | ||
| [self.methodChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, set the backing ivar instead of using self property getter in an initializer
| // @"endIndex" : 5, | ||
| // @"suggestions": [@"suggestion1", @"suggestion2"..] | ||
| // } | ||
| - (NSDictionary*)kInitiateSpellCheck:(id)args { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't match the static NSString constant.
| - (NSDictionary*)kInitiateSpellCheck:(id)args { | |
| - (NSDictionary*)initiateSpellCheck:(id)args { |
| startingAt:0 | ||
| wrap:NO | ||
| language:language]; | ||
| if (misspelledRange.length <= 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's more idiomatic to check for location != NSNotFound but length != 0 is fine too. You don't need <=0 since length is a NSUInteger though.
| } | ||
|
|
||
| - (void)dealloc { | ||
| [self.textChecker release]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| [self.textChecker release]; | |
| [_textChecker release]; |
| // @"suggestions": [@"suggestion1", @"suggestion2"..] | ||
| // } | ||
| - (NSDictionary*)kInitiateSpellCheck:(id)args { | ||
| NSString* language = args[0]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this check if this is in availableLanguages and either bail if not, or allow nil and fall back to the first availableLanguages preference? Doc says https://developer.apple.com/documentation/uikit/uitextchecker/1621033-availablelanguages?language=objc
An array of strings representing ISO 639-1 language codes or combined ISO 639-1 language codes and ISO 3166-1 regional codes (for example, en_US).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. we should check this.
I prefer bailing rather than assuming a language for the user.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I can do some tests with an "unavailable" language and see what the return values are for rangeOfMisspelledWordInString. the result might just be as the same as bailing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The return value is (0, 0), so using a "unsupported" language is the same as "there is no spell checking error", which seems make sense to me.
The worry is that it makes harder for user to notice if they use a "unsupported" language. I think it is still necessary for us to check for it, and return an error in the method channel.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update:
No suggestions now returns an empty array
Unsupported language now returns nil.
jmagman
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nits, LGTM
| result([self initiateSpellCheck:args]); | ||
| FlutterError* error; | ||
| NSDictionary* spellCheckResult = [self initiateSpellCheck:args withError:&error]; | ||
| if (error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's more idiomatic in Objective-C to check if the return value is nil, and then process the error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A returned nil value is valid without an error in our case here: that is when spell-check didn't find a misspelled word.
So if we check nil first, it will be:
if (! spellCheckResult && error) {
...
} else {
...
}That's essentially the same with an extra unnecessary check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or maybe initiateSpellCheck: should return something else to indicate a spell check didn't find a misspelled word?
Since NSNotFound is not defined by us, it would be error prone to return NSNotFound to dart and rely on that. Maybe we can use -1 as the startingIndex when no misspelled word is found? So:
{
startIndex: -1
endIndex: -1
suggestions: []
}
This way, initiateSpellCheck: only returns nil when there's an error, which better matches Objective-C idiomatic. WDYT
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A returned nil value is valid without an error in our case here: that is when spell-check didn't find a misspelled word.
Can we make that case return an empty dictionary instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I will change it to empty array as we are changing this API to return all the spell errors.
| capturedResult = result; | ||
| }]; | ||
| XCTAssertTrue([capturedResult isKindOfClass:[FlutterError class]]); | ||
| XCTAssertEqual(((FlutterError*)capturedResult).code, @"unsupported_language"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| XCTAssertEqual(((FlutterError*)capturedResult).code, @"unsupported_language"); | |
| XCTAssertEqualObjects((FlutterError*)capturedResult).code, @"unsupported_language"); |
| // @"endIndex" : 5, | ||
| // @"suggestions": [@"suggestion1", @"suggestion2"..] | ||
| // } | ||
| - (NSDictionary*)initiateSpellCheck:(id)args withError:(FlutterError**)error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| - (NSDictionary*)initiateSpellCheck:(id)args withError:(FlutterError**)error { | |
| - (NSDictionary<NSString *, id>*)initiateSpellCheck:(id)args withError:(FlutterError**)error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually this name is kind of confusing, it's not like it's kicking off a spell check to be processed later.
How about something like spellCheckSuggestionsForText:inLanguage:withError: where the arg indexing happens one layer up?
shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.mm
Outdated
Show resolved
Hide resolved
|
@chinmaygarde Thank you for the reminder:) I switched my attention and started banging my head on flutter/flutter#103076 as it has a higher priority. I will circle back to this PR once I have some progress on the other issue. |
cyanglaz
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jmagman Updated to return all spell check errors in the String.
PTAL
| result([self initiateSpellCheck:args]); | ||
| FlutterError* error; | ||
| NSDictionary* spellCheckResult = [self initiateSpellCheck:args withError:&error]; | ||
| if (error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I will change it to empty array as we are changing this API to return all the spell errors.
| // @"suggestions": [@"suggestion1", @"suggestion2"..] | ||
| // } | ||
| - (NSDictionary*)kInitiateSpellCheck:(id)args { | ||
| NSString* language = args[0]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update:
No suggestions now returns an empty array
Unsupported language now returns nil.
| self.flutterSpellCheckPlugin = | ||
| [[[FlutterSpellCheckPlugin alloc] initWithMethodChannel:_spellCheckChannel.get()] retain]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❌ Failures for clang-tidy on /opt/s/w/ir/cache/builder/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm:
/opt/s/w/ir/cache/builder/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm:592:1: error: Potential leak of an object of type 'FlutterSpellCheckPlugin *' [clang-analyzer-osx.cocoa.RetainCount,-warnings-as-errors]
}
^
/opt/s/w/ir/cache/builder/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm:528:7: note: Assuming the condition is false
if ([_initialRoute length] > 0) {
^~~~~~~~~~~~~~~~~~~~~~~~~~
/opt/s/w/ir/cache/builder/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm:528:3: note: Taking false branch
if ([_initialRoute length] > 0) {
^
/opt/s/w/ir/cache/builder/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm:591:8: note: Method returns an instance of FlutterSpellCheckPlugin with a +1 retain count
[[[FlutterSpellCheckPlugin alloc] initWithMethodChannel:_spellCheckChannel.get()] retain];
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/opt/s/w/ir/cache/builder/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm:591:7: note: Reference count incremented. The object now has a +2 retain count
[[[FlutterSpellCheckPlugin alloc] initWithMethodChannel:_spellCheckChannel.get()] retain];
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/opt/s/w/ir/cache/builder/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm:592:1: note: Object leaked: allocated object of type 'FlutterSpellCheckPlugin *' is not referenced later in this execution path and has a retain count of +2
}
| #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.h" | ||
|
|
||
| #import <Foundation/Foundation.h> | ||
| #import <UIKit/UIApplication.h> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| #import <UIKit/UIApplication.h> |
This is already imported with #import <UIKit/UIKit.h>
| self = [super init]; | ||
| if (self) { | ||
| [_methodChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { | ||
| [self handleMethodCall:call result:result]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this isn't a retain cycle in MRC since it doesn't increase the retain count. Does it need to check if self is nil? Brain hurts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! We should check if self is nil.
| startingAt:(NSInteger)startingOffset | ||
| wrap:(BOOL)wrapFlag | ||
| language:(NSString*)language { | ||
| return self.startingIndexToRange[@(startingOffset)].rangeValue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NSRangeFromString is already a thing.
| FML_DCHECK([UITextChecker.availableLanguages containsObject:language]); | ||
| NSRange misspelledRange = | ||
| [self.textChecker rangeOfMisspelledWordInString:text | ||
| range:NSMakeRange(0, text.length) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this API handle empty/nil strings?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It handles empty String, which I added a test for.
We won't get a nil here as I have added some assert above.
| NSArray* suggestions1 = @[ @"suggestion 1", @"suggestion 2" ]; | ||
| NSArray* suggestions2 = @[ @"suggestion 3", @"suggestion 4" ]; | ||
| [self mockUITextCheckerWithExpectedMisspelledWordRange:NSMakeRange(0, 5) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like you're mostly testing out your mocks here. Are you sure it's thoroughly testing the corner cases of the range math, like the last part of the string being misspelled, that every letter is included in the ranges, empty strings?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
like the last part of the string being misspelled
Example: "This is missspelled" where the last word is being tested.
that every letter is included in the ranges
Example "missspelled" where the entire string is one misspelled word.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Offline discussion: we need one more test to check the while loop stops when nextStartingIndex is out of bounds.
| nextOffset = [nextSpellSuggestion[@"location"] integerValue] + | ||
| [nextSpellSuggestion[@"length"] integerValue]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are NSUIntegers.
| result[@"location"] = @(misspelledRange.location); | ||
| result[@"length"] = @(misspelledRange.length); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of splitting the range you can use NSValue, see valueWithRange:. Or better, create a new object with range and suggestions properties? Or a dictionary with the NSValue as a key and the suggestions. The string keys needed between two methods seems unnecessary.
|
Friendly ping @cyanglaz to check out the clang-tidy complaint. |
| NSString* method = call.method; | ||
| NSArray* args = call.arguments; | ||
| if ([method isEqualToString:kInitiateSpellCheck]) { | ||
| FML_DCHECK(args.count == 2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assert both fields cannot be nil.
They can still be NSNull if dart passes null to this method channel. I added a bail below when either of them is NSNull
| FML_DCHECK([UITextChecker.availableLanguages containsObject:language]); | ||
| NSRange misspelledRange = | ||
| [self.textChecker rangeOfMisspelledWordInString:text | ||
| range:NSMakeRange(0, text.length) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It handles empty String, which I added a test for.
We won't get a nil here as I have added some assert above.
cyanglaz
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jmagman Updated, ptal
| NSArray* suggestions1 = @[ @"suggestion 1", @"suggestion 2" ]; | ||
| NSArray* suggestions2 = @[ @"suggestion 3", @"suggestion 4" ]; | ||
| [self mockUITextCheckerWithExpectedMisspelledWordRange:NSMakeRange(0, 5) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Offline discussion: we need one more test to check the while loop stops when nextStartingIndex is out of bounds.
| nextOffset = | ||
| nextSpellSuggestion.misspelledRange.location + nextSpellSuggestion.misspelledRange.length; | ||
| } | ||
| } while (nextSpellSuggestion != nil && nextOffset < text.length); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nextOffset < text.length :)
| // stop if nsnotfound | ||
|
|
||
| // if the startingIndex is last | ||
|
|
||
| // Test to make sure the while loop that checks all the misspelled word stops when the a | ||
| // `NSNotFound` is found. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably didn't mean to leave this in 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't mean to leave the first 2 lines in but i want to leave the "Test to make sure.." in. This serves as a little explanation to the test as what logic is the test testings.
| suggestions:(NSArray<NSString*>*)suggestions { | ||
| self = [super init]; | ||
| if (self) { | ||
| _suggestions = suggestions; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| _suggestions = suggestions; | |
| _suggestions = [suggestions copy]; |
| result[@"location"] = @(_misspelledRange.location); | ||
| result[@"length"] = @(_misspelledRange.length); | ||
| result[@"suggestions"] = [_suggestions copy]; | ||
| return result; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❌ Failures for clang-tidy on /opt/s/w/ir/cache/builder/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.mm:
/opt/s/w/ir/cache/builder/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.mm:159:3: error: Potential leak of an object of type 'id' [clang-analyzer-osx.cocoa.RetainCount,-warnings-as-errors]
return result;
^~~~~~~~~~~~~
/opt/s/w/ir/cache/builder/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.mm:158:28: note: Method returns an Objective-C object with a +1 retain count
result[@"suggestions"] = [_suggestions copy];
^~~~~~~~~~~~~~~~~~~
/opt/s/w/ir/cache/builder/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.mm:159:3: note: Object leaked: allocated object of type 'id' is not referenced later in this execution path and has a retain count of +1
return result;
^~~~~~~~~~~~~
draft spellcheck objc review throw error if language is not supported run spell check to the end of the string convert to use scoped_nsobject to match other plugins some review fixes review clang-tidy, remove unnecessary comments
| _spellCheckChannel.reset([[FlutterMethodChannel alloc] | ||
| initWithName:@"flutter/spellcheck" | ||
| binaryMessenger:self.binaryMessenger | ||
| codec:[FlutterJSONMethodCodec sharedInstance]]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
drive by: new codecs should avoid using the JSON codec.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What codec should be used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FlutterStandardMethodCodec
This reverts commit cba7bf3.
Adds SpellCheck Objective C code in engine. This is part of flutter/flutter#34688
The PR can be landed standalone, it does not affect or add features to the current Flutter.
This PR adds a
FlutterSpellCheckPlugintoFlutterEngine. This plugin uses a method channel to communicate with dart. The plugin currently has one method:SpellCheck.initiateSpellCheck, which looks for the first misspelled word in the argument String and returns a result map:Design doc: https://docs.google.com/document/d/10Lvkz802QpzAGFi3Q9WVTMVUV-SPJyG3_xs8XfxG59Q/edit?usp=sharing
Pre-launch Checklist
writing and running engine tests.
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.