forked from flutter/engine
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathFlutterSpellCheckPlugin.mm
More file actions
162 lines (136 loc) · 5.89 KB
/
FlutterSpellCheckPlugin.mm
File metadata and controls
162 lines (136 loc) · 5.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
// 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, retain) UITextChecker* textChecker;
@end
@implementation FlutterSpellCheckPlugin
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if (!_textChecker) {
// UITextChecker is an expensive object to initiate, see:
// https://github.com/flutter/flutter/issues/104454. Lazily initialate the UITextChecker object
// until at first method channel call. We avoid using lazy getter for testing.
_textChecker = [[UITextChecker alloc] init];
}
NSString* method = call.method;
NSArray* args = call.arguments;
if ([method isEqualToString:kInitiateSpellCheck]) {
FML_DCHECK(args.count == 2);
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 an NSArray.
// Each item of the array is a dictionary representing a misspelled word and suggestions.
// The format looks like:
// {
// startIndex: 0,
// endIndex: 5,
// suggestions: [hello, ...]
// }
//
// 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 {
// Transform Dart Locale format to iOS language format.
language = [language stringByReplacingOccurrencesOfString:@"-" withString:@"_"];
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);
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)
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*, NSObject*>*)toDictionary {
NSMutableDictionary* result = [[[NSMutableDictionary alloc] initWithCapacity:3] autorelease];
result[@"startIndex"] = @(_misspelledRange.location);
// The end index represents the next index after the last character of a misspelled word to match
// the behavior of Dart's TextRange: https://api.flutter.dev/flutter/dart-ui/TextRange/end.html
result[@"endIndex"] = @(_misspelledRange.location + _misspelledRange.length);
result[@"suggestions"] = _suggestions;
return result;
}
- (void)dealloc {
[_suggestions release];
[super dealloc];
}
@end