Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -1661,6 +1661,9 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterRestora
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterRestorationPluginTest.mm
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.mm
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.h
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.mm
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPluginTest.mm
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
Expand Down
3 changes: 3 additions & 0 deletions shell/platform/darwin/ios/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ source_set("flutter_framework_source") {
"framework/Source/FlutterRestorationPlugin.mm",
"framework/Source/FlutterSemanticsScrollView.h",
"framework/Source/FlutterSemanticsScrollView.mm",
"framework/Source/FlutterSpellCheckPlugin.h",
"framework/Source/FlutterSpellCheckPlugin.mm",
"framework/Source/FlutterTextInputDelegate.h",
"framework/Source/FlutterTextInputPlugin.h",
"framework/Source/FlutterTextInputPlugin.mm",
Expand Down Expand Up @@ -264,6 +266,7 @@ shared_library("ios_test_flutter") {
"framework/Source/FlutterKeyboardManagerTest.mm",
"framework/Source/FlutterPluginAppLifeCycleDelegateTest.mm",
"framework/Source/FlutterRestorationPluginTest.mm",
"framework/Source/FlutterSpellCheckPluginTest.mm",
"framework/Source/FlutterTextInputPluginTest.mm",
"framework/Source/FlutterUndoManagerPluginTest.mm",
"framework/Source/FlutterViewControllerTest.mm",
Expand Down
14 changes: 14 additions & 0 deletions shell/platform/darwin/ios/framework/Source/FlutterEngine.mm
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterIndirectScribbleDelegate.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterUndoManagerDelegate.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterUndoManagerPlugin.h"
Expand Down Expand Up @@ -111,6 +112,7 @@ @implementation FlutterEngine {
fml::scoped_nsobject<FlutterPlatformPlugin> _platformPlugin;
fml::scoped_nsobject<FlutterTextInputPlugin> _textInputPlugin;
fml::scoped_nsobject<FlutterUndoManagerPlugin> _undoManagerPlugin;
fml::scoped_nsobject<FlutterSpellCheckPlugin> _spellCheckPlugin;
fml::scoped_nsobject<FlutterRestorationPlugin> _restorationPlugin;
fml::scoped_nsobject<FlutterMethodChannel> _localizationChannel;
fml::scoped_nsobject<FlutterMethodChannel> _navigationChannel;
Expand All @@ -119,6 +121,7 @@ @implementation FlutterEngine {
fml::scoped_nsobject<FlutterMethodChannel> _platformViewsChannel;
fml::scoped_nsobject<FlutterMethodChannel> _textInputChannel;
fml::scoped_nsobject<FlutterMethodChannel> _undoManagerChannel;
fml::scoped_nsobject<FlutterMethodChannel> _spellCheckChannel;
fml::scoped_nsobject<FlutterBasicMessageChannel> _lifecycleChannel;
fml::scoped_nsobject<FlutterBasicMessageChannel> _systemChannel;
fml::scoped_nsobject<FlutterBasicMessageChannel> _settingsChannel;
Expand Down Expand Up @@ -464,6 +467,9 @@ - (FlutterMethodChannel*)textInputChannel {
- (FlutterMethodChannel*)undoManagerChannel {
return _undoManagerChannel.get();
}
- (FlutterMethodChannel*)spellCheckChannel {
return _spellCheckChannel.get();
}
- (FlutterBasicMessageChannel*)lifecycleChannel {
return _lifecycleChannel.get();
}
Expand Down Expand Up @@ -493,6 +499,7 @@ - (void)resetChannels {
_systemChannel.reset();
_settingsChannel.reset();
_keyEventChannel.reset();
_spellCheckChannel.reset();
}

- (void)startProfiler {
Expand Down Expand Up @@ -561,6 +568,11 @@ - (void)setupChannels {
binaryMessenger:self.binaryMessenger
codec:[FlutterJSONMethodCodec sharedInstance]]);

_spellCheckChannel.reset([[FlutterMethodChannel alloc]
initWithName:@"flutter/spellcheck"
binaryMessenger:self.binaryMessenger
codec:[FlutterJSONMethodCodec sharedInstance]]);
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

Choose a reason for hiding this comment

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

FlutterStandardMethodCodec


_lifecycleChannel.reset([[FlutterBasicMessageChannel alloc]
initWithName:@"flutter/lifecycle"
binaryMessenger:self.binaryMessenger
Expand Down Expand Up @@ -595,6 +607,8 @@ - (void)setupChannels {
_restorationPlugin.reset([[FlutterRestorationPlugin alloc]
initWithChannel:_restorationChannel.get()
restorationEnabled:_restorationEnabled]);
_spellCheckPlugin.reset(
[[FlutterSpellCheckPlugin alloc] initWithMethodChannel:_spellCheckChannel.get()]);
}

- (void)maybeSetupPlatformViewChannels {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERSPELLCHECKPLUGIN_H_
#define SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERSPELLCHECKPLUGIN_H_

#include "flutter/fml/memory/weak_ptr.h"

#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h"
#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h"

@interface FlutterSpellCheckPlugin : NSObject
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)initWithMethodChannel:(FlutterMethodChannel*)methodChannel
NS_DESIGNATED_INITIALIZER;

@end

@interface FlutterSpellCheckResult : NSObject

@property(nonatomic, copy, readonly) NSArray<NSString*>* suggestions;
@property(nonatomic, assign, readonly) NSRange misspelledRange;

- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)initWithMisspelledRange:(NSRange)range
suggestions:(NSArray<NSString*>*)suggestions NS_DESIGNATED_INITIALIZER;
- (NSDictionary<NSString*, id>*)toDictionary;

@end

#endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERSPELLCHECKPLUGIN_H_
167 changes: 167 additions & 0 deletions shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.h"

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

#import "flutter/fml/logging.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"

// Method Channel name to start spell check.
static NSString* const kInitiateSpellCheck = @"SpellCheck.initiateSpellCheck";

@interface FlutterSpellCheckPlugin ()

@property(nonatomic, assign) FlutterMethodChannel* methodChannel;
@property(nonatomic, retain) UITextChecker* textChecker;

@end

@implementation FlutterSpellCheckPlugin

- (instancetype)initWithMethodChannel:(FlutterMethodChannel*)methodChannel {
self = [super init];
if (self) {
[_methodChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
if (!self) {
return;
}
[self handleMethodCall:call result:result];
Copy link
Member

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.

Copy link
Contributor Author

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.

}];
_textChecker = [[UITextChecker alloc] init];
}
return self;
}

- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
NSString* method = call.method;
NSArray* args = call.arguments;
if ([method isEqualToString:kInitiateSpellCheck]) {
FML_DCHECK(args.count == 2);
Copy link
Contributor Author

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

id language = args[0];
id text = args[1];
if (language == [NSNull null] || text == [NSNull null]) {
// Bail if null arguments are passed from dart.
result(nil);
return;
}

NSArray<NSDictionary<NSString*, id>*>* spellCheckResult =
[self findAllSpellCheckSuggestionsForText:text inLanguage:language];
result(spellCheckResult);
}
}

// Get all the misspelled words and suggestions in the entire String.
//
// The result will be formatted as am NSArray.
// Each item of the array is a representation of a misspelled word and suggestions.
// The format of each item looks like this:
// {
// @"location": 0,
// @"length" : 5,
// @"suggestions": [@"suggestion1", @"suggestion2"..]
// }
//
// Returns nil if the language is invalid.
// Returns an empty array if no spell check suggestions.
- (NSArray<NSDictionary<NSString*, id>*>*)findAllSpellCheckSuggestionsForText:(NSString*)text
inLanguage:(NSString*)language {
if (![UITextChecker.availableLanguages containsObject:language]) {
return nil;
}

NSMutableArray<FlutterSpellCheckResult*>* allSpellSuggestions = [[NSMutableArray alloc] init];

FlutterSpellCheckResult* nextSpellSuggestion;
NSUInteger nextOffset = 0;
do {
nextSpellSuggestion = [self findSpellCheckSuggestionsForText:text
inLanguage:language
startingOffset:nextOffset];
if (nextSpellSuggestion != nil) {
[allSpellSuggestions addObject:nextSpellSuggestion];
nextOffset =
nextSpellSuggestion.misspelledRange.location + nextSpellSuggestion.misspelledRange.length;
}
} while (nextSpellSuggestion != nil && nextOffset < text.length);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

nextOffset < text.length :)


NSMutableArray* methodChannelResult = [[[NSMutableArray alloc] init] autorelease];

for (FlutterSpellCheckResult* result in allSpellSuggestions) {
[methodChannelResult addObject:[result toDictionary]];
}

[allSpellSuggestions release];
return methodChannelResult;
}

// Get the misspelled word and suggestions.
//
// Returns nil if no spell check suggestions.
- (FlutterSpellCheckResult*)findSpellCheckSuggestionsForText:(NSString*)text
inLanguage:(NSString*)language
startingOffset:(NSInteger)startingOffset {
FML_DCHECK([UITextChecker.availableLanguages containsObject:language]);
NSRange misspelledRange =
[self.textChecker rangeOfMisspelledWordInString:text
range:NSMakeRange(0, text.length)
Copy link
Member

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?

Copy link
Contributor Author

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.

startingAt:startingOffset
wrap:NO
language:language];
if (misspelledRange.location == NSNotFound) {
// No misspelled word found
return nil;
}

// If no possible guesses, the API returns an empty array:
// https://developer.apple.com/documentation/uikit/uitextchecker/1621037-guessesforwordrange?language=objc
NSArray<NSString*>* suggestions = [self.textChecker guessesForWordRange:misspelledRange
inString:text
language:language];
FlutterSpellCheckResult* result =
[[[FlutterSpellCheckResult alloc] initWithMisspelledRange:misspelledRange
suggestions:suggestions] autorelease];
return result;
}

- (UITextChecker*)textChecker {
return _textChecker;
}

- (void)dealloc {
[_textChecker release];
[super dealloc];
}

@end

@implementation FlutterSpellCheckResult

- (instancetype)initWithMisspelledRange:(NSRange)range
suggestions:(NSArray<NSString*>*)suggestions {
self = [super init];
if (self) {
_suggestions = [suggestions copy];
_misspelledRange = range;
}
return self;
}

- (NSDictionary<NSString*, id>*)toDictionary {
NSMutableDictionary* result = [[[NSMutableDictionary alloc] initWithCapacity:3] autorelease];
result[@"location"] = @(_misspelledRange.location);
result[@"length"] = @(_misspelledRange.length);
result[@"suggestions"] = [[_suggestions copy] autorelease];
return result;
Copy link
Member

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;
^~~~~~~~~~~~~

}

- (void)dealloc {
[_suggestions release];
[super dealloc];
}

@end
Loading