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

Commit a560c26

Browse files
authored
fix voice control delete line command does not delete line correctly (#24831)
1 parent afc72ee commit a560c26

File tree

3 files changed

+119
-1
lines changed

3 files changed

+119
-1
lines changed

shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@
4444

4545
@end
4646

47+
/** A tokenizer used by `FlutterTextInputView` to customize string parsing. */
48+
@interface FlutterTokenizer : UITextInputStringTokenizer
49+
@end
50+
4751
#if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG
4852
FLUTTER_DARWIN_EXPORT
4953
#endif

shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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
}

shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,16 @@ - (NSMutableDictionary*)mutableTemplateCopy {
133133
[FlutterTextInputView class]]];
134134
}
135135

136+
- (FlutterTextRange*)getLineRangeFromTokenizer:(id<UITextInputTokenizer>)tokenizer
137+
atIndex:(NSInteger)index {
138+
UITextRange* range =
139+
[tokenizer rangeEnclosingPosition:[FlutterTextPosition positionWithIndex:index]
140+
withGranularity:UITextGranularityLine
141+
inDirection:UITextLayoutDirectionRight];
142+
XCTAssertTrue([range isKindOfClass:[FlutterTextRange class]]);
143+
return (FlutterTextRange*)range;
144+
}
145+
136146
#pragma mark - Tests
137147

138148
- (void)testSecureInput {
@@ -818,4 +828,41 @@ - (void)testFlutterTextInputViewDirectFocusToBackingTextInput {
818828
XCTAssertEqual(inputView.receivedNotificationTarget, backing);
819829
}
820830

831+
- (void)testFlutterTokenizerCanParseLines {
832+
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init];
833+
inputView.textInputDelegate = engine;
834+
id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
835+
836+
// The tokenizer returns zero range When text is empty.
837+
FlutterTextRange* range = [self getLineRangeFromTokenizer:tokenizer atIndex:0];
838+
XCTAssertEqual(range.range.location, 0u);
839+
XCTAssertEqual(range.range.length, 0u);
840+
841+
[inputView insertText:@"how are you\nI am fine, Thank you"];
842+
843+
range = [self getLineRangeFromTokenizer:tokenizer atIndex:0];
844+
XCTAssertEqual(range.range.location, 0u);
845+
XCTAssertEqual(range.range.length, 11u);
846+
847+
range = [self getLineRangeFromTokenizer:tokenizer atIndex:2];
848+
XCTAssertEqual(range.range.location, 0u);
849+
XCTAssertEqual(range.range.length, 11u);
850+
851+
range = [self getLineRangeFromTokenizer:tokenizer atIndex:11];
852+
XCTAssertEqual(range.range.location, 0u);
853+
XCTAssertEqual(range.range.length, 11u);
854+
855+
range = [self getLineRangeFromTokenizer:tokenizer atIndex:12];
856+
XCTAssertEqual(range.range.location, 12u);
857+
XCTAssertEqual(range.range.length, 20u);
858+
859+
range = [self getLineRangeFromTokenizer:tokenizer atIndex:15];
860+
XCTAssertEqual(range.range.location, 12u);
861+
XCTAssertEqual(range.range.length, 20u);
862+
863+
range = [self getLineRangeFromTokenizer:tokenizer atIndex:32];
864+
XCTAssertEqual(range.range.location, 12u);
865+
XCTAssertEqual(range.range.length, 20u);
866+
}
867+
821868
@end

0 commit comments

Comments
 (0)