@@ -378,6 +378,73 @@ - (BOOL)isEqualTo:(FlutterTextRange*)other {
378378}
379379@end
380380
381+ #pragma mark - FlutterTokenizer
382+
383+ @interface FlutterTokenizer ()
384+
385+ @property (nonatomic , assign ) FlutterTextInputView* textInputView;
386+
387+ @end
388+
389+ @implementation FlutterTokenizer
390+
391+ - (instancetype )initWithTextInput : (UIResponder<UITextInput>*)textInput {
392+ NSAssert ([textInput isKindOfClass: [FlutterTextInputView class ]],
393+ @" The FlutterTokenizer can only be used in a FlutterTextInputView" );
394+ self = [super initWithTextInput: textInput];
395+ if (self) {
396+ _textInputView = (FlutterTextInputView*)textInput;
397+ }
398+ return self;
399+ }
400+
401+ - (UITextRange*)rangeEnclosingPosition : (UITextPosition*)position
402+ withGranularity : (UITextGranularity)granularity
403+ inDirection : (UITextDirection)direction {
404+ UITextRange* result;
405+ switch (granularity) {
406+ case UITextGranularityLine:
407+ // The default UITextInputStringTokenizer does not handle line granularity
408+ // correctly. We need to implement our own line tokenizer.
409+ result = [self lineEnclosingPosition: position];
410+ break ;
411+ case UITextGranularityCharacter:
412+ case UITextGranularityWord:
413+ case UITextGranularitySentence:
414+ case UITextGranularityParagraph:
415+ case UITextGranularityDocument:
416+ // The UITextInputStringTokenizer can handle all these cases correctly.
417+ result = [super rangeEnclosingPosition: position
418+ withGranularity: granularity
419+ inDirection: direction];
420+ break ;
421+ }
422+ return result;
423+ }
424+
425+ - (UITextRange*)lineEnclosingPosition : (UITextPosition*)position {
426+ // Gets the first line break position after the input position.
427+ NSString * textAfter = [_textInputView
428+ textInRange: [_textInputView textRangeFromPosition: position
429+ toPosition: [_textInputView endOfDocument ]]];
430+ NSArray <NSString *>* linesAfter = [textAfter componentsSeparatedByString: @" \n " ];
431+ NSInteger offSetToLineBreak = [linesAfter firstObject ].length ;
432+ UITextPosition* lineBreakAfter = [_textInputView positionFromPosition: position
433+ offset: offSetToLineBreak];
434+ // Gets the first line break position before the input position.
435+ NSString * textBefore = [_textInputView
436+ textInRange: [_textInputView textRangeFromPosition: [_textInputView beginningOfDocument ]
437+ toPosition: position]];
438+ NSArray <NSString *>* linesBefore = [textBefore componentsSeparatedByString: @" \n " ];
439+ NSInteger offSetFromLineBreak = [linesBefore lastObject ].length ;
440+ UITextPosition* lineBreakBefore = [_textInputView positionFromPosition: position
441+ offset: -offSetFromLineBreak];
442+
443+ return [_textInputView textRangeFromPosition: lineBreakBefore toPosition: lineBreakAfter];
444+ }
445+
446+ @end
447+
381448// A FlutterTextInputView that masquerades as a UITextField, and forwards
382449// selectors it can't respond to to a shared UITextField instance.
383450//
@@ -629,7 +696,7 @@ - (BOOL)canBecomeFirstResponder {
629696
630697- (id <UITextInputTokenizer>)tokenizer {
631698 if (_tokenizer == nil ) {
632- _tokenizer = [[UITextInputStringTokenizer alloc ] initWithTextInput: self ];
699+ _tokenizer = [[FlutterTokenizer alloc ] initWithTextInput: self ];
633700 }
634701 return _tokenizer;
635702}
0 commit comments