// // KBBackspaceUndoManager.m // CustomKeyboard // #import "KBBackspaceUndoManager.h" #import "KBResponderUtils.h" NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspaceUndoStateDidChangeNotification"; @interface KBBackspaceUndoManager () @property (nonatomic, strong) NSMutableArray *segments; // deletion order (last -> first) @property (nonatomic, assign) BOOL lastActionWasClear; @property (nonatomic, assign) BOOL hasUndo; @end @implementation KBBackspaceUndoManager + (instancetype)shared { static KBBackspaceUndoManager *mgr = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ mgr = [[KBBackspaceUndoManager alloc] init]; }); return mgr; } - (instancetype)init { if (self = [super init]) { _segments = [NSMutableArray array]; } return self; } - (void)recordClearWithContext:(NSString *)context { if (context.length == 0) { return; } NSString *segment = [self kb_segmentForClearFromContext:context]; if (segment.length == 0) { return; } if (!self.lastActionWasClear) { [self.segments removeAllObjects]; } [self.segments addObject:segment]; self.lastActionWasClear = YES; [self kb_updateHasUndo:YES]; } - (void)performUndoFromResponder:(UIResponder *)responder { if (self.segments.count == 0) { return; } UIInputViewController *ivc = KBFindInputViewController(responder); if (!ivc) { return; } id proxy = ivc.textDocumentProxy; NSString *text = [self kb_buildUndoText]; if (text.length == 0) { return; } [proxy insertText:text]; [self.segments removeAllObjects]; self.lastActionWasClear = NO; [self kb_updateHasUndo:NO]; } - (void)registerNonClearAction { self.lastActionWasClear = NO; if (self.segments.count == 0) { return; } [self.segments removeAllObjects]; [self kb_updateHasUndo:NO]; } #pragma mark - Helpers - (void)kb_updateHasUndo:(BOOL)hasUndo { if (self.hasUndo == hasUndo) { return; } self.hasUndo = hasUndo; [[NSNotificationCenter defaultCenter] postNotificationName:KBBackspaceUndoStateDidChangeNotification object:self]; } - (NSString *)kb_segmentForClearFromContext:(NSString *)context { NSInteger length = context.length; if (length == 0) { return @""; } static NSCharacterSet *sentenceBoundarySet = nil; static NSCharacterSet *whitespaceSet = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?;。!?;…\n"]; whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet]; }); NSInteger end = length; while (end > 0) { unichar ch = [context characterAtIndex:end - 1]; if ([whitespaceSet characterIsMember:ch]) { end -= 1; } else { break; } } NSInteger searchEnd = end; while (searchEnd > 0) { unichar ch = [context characterAtIndex:searchEnd - 1]; if ([sentenceBoundarySet characterIsMember:ch]) { searchEnd -= 1; } else { break; } } NSInteger boundaryIndex = NSNotFound; for (NSInteger i = searchEnd - 1; i >= 0; i--) { unichar ch = [context characterAtIndex:i]; if ([sentenceBoundarySet characterIsMember:ch]) { boundaryIndex = i; break; } } NSInteger start = (boundaryIndex == NSNotFound) ? 0 : (boundaryIndex + 1); if (start >= length) { return @""; } return [context substringFromIndex:start]; } - (NSString *)kb_buildUndoText { if (self.segments.count == 0) { return @""; } NSArray *ordered = [[self.segments reverseObjectEnumerator] allObjects]; NSMutableString *result = [NSMutableString string]; for (NSInteger i = 0; i < ordered.count; i++) { NSString *segment = ordered[i] ?: @""; if (segment.length == 0) { continue; } if (i < ordered.count - 1) { segment = [self kb_replaceTrailingBoundaryWithComma:segment]; } [result appendString:segment]; } return result; } - (NSString *)kb_replaceTrailingBoundaryWithComma:(NSString *)segment { if (segment.length == 0) { return segment; } static NSCharacterSet *boundarySet = nil; static NSCharacterSet *englishBoundarySet = nil; static NSCharacterSet *whitespaceSet = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ boundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?;。!?;…\n"]; englishBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?;"]; whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet]; }); NSInteger idx = segment.length - 1; while (idx >= 0) { unichar ch = [segment characterAtIndex:idx]; if ([whitespaceSet characterIsMember:ch]) { idx -= 1; continue; } if (![boundarySet characterIsMember:ch]) { return segment; } NSString *comma = [englishBoundarySet characterIsMember:ch] ? @"," : @","; NSMutableString *mutable = [segment mutableCopy]; NSRange r = NSMakeRange(idx, 1); [mutable replaceCharactersInRange:r withString:comma]; return mutable; } return segment; } @end