#import "KBInputBufferManager.h" #import #if DEBUG static NSString *KBLogString2(NSString *tag, NSString *text) { NSString *safeTag = tag ?: @""; NSString *safeText = text ?: @""; if (safeText.length <= 2000) { return [NSString stringWithFormat:@"[%@] len=%lu text=%@", safeTag, (unsigned long)safeText.length, safeText]; } NSString *head = [safeText substringToIndex:800]; NSString *tail = [safeText substringFromIndex:safeText.length - 800]; return [NSString stringWithFormat:@"[%@] len=%lu head=%@ ... tail=%@", safeTag, (unsigned long)safeText.length, head, tail]; } #define KB_BUF_LOG(tag, text) NSLog(@"❤️=%@", KBLogString2((tag), (text))) #else #define KB_BUF_LOG(tag, text) do {} while(0) #endif @interface KBInputBufferManager () @property (nonatomic, copy, readwrite) NSString *liveText; @property (nonatomic, copy, readwrite) NSString *manualSnapshot; @property (nonatomic, copy, readwrite) NSString *pendingClearSnapshot; @property (nonatomic, assign) BOOL manualSnapshotDirty; @end @implementation KBInputBufferManager + (instancetype)shared { static KBInputBufferManager *mgr = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ mgr = [[KBInputBufferManager alloc] init]; }); return mgr; } - (instancetype)init { if (self = [super init]) { _liveText = @""; _manualSnapshot = @""; _pendingClearSnapshot = @""; _manualSnapshotDirty = NO; } return self; } - (void)seedIfEmptyWithContextBefore:(NSString *)before after:(NSString *)after { if (self.liveText.length > 0 || self.manualSnapshot.length > 0) { return; } NSString *safeBefore = before ?: @""; NSString *safeAfter = after ?: @""; NSString *full = [safeBefore stringByAppendingString:safeAfter]; if (full.length == 0) { return; } self.liveText = full; self.manualSnapshot = full; self.manualSnapshotDirty = NO; KB_BUF_LOG(@"seedIfEmpty", full); } - (void)updateFromExternalContextBefore:(NSString *)before after:(NSString *)after { NSString *safeBefore = before ?: @""; NSString *safeAfter = after ?: @""; NSString *context = [safeBefore stringByAppendingString:safeAfter]; if (context.length == 0) { return; } // 微信/QQ 等宿主通常只提供光标附近“截断窗口”,不应当作为全文快照。 // 这里只更新 liveText,给删除/清空逻辑做参考;manualSnapshot 仅由键盘自身输入/撤销来维护。 self.liveText = context; self.manualSnapshotDirty = YES; #if DEBUG static NSUInteger sExternalLogCounter = 0; sExternalLogCounter += 1; if (sExternalLogCounter % 12 == 0) { KB_BUF_LOG(@"updateFromExternalContext/liveOnly", context); } #endif } - (void)refreshFromProxyIfPossible:(id)proxy { NSString *harvested = [self kb_harvestFullTextFromProxy:proxy]; if (harvested.length == 0) { KB_BUF_LOG(@"refreshFromProxy/failedOrUnsupported", @""); return; } BOOL manualEmpty = (self.manualSnapshot.length == 0); BOOL longerThanManual = (harvested.length > self.manualSnapshot.length); if (!(manualEmpty || longerThanManual)) { KB_BUF_LOG(@"refreshFromProxy/ignoredShorter", harvested); return; } self.liveText = harvested; self.manualSnapshot = harvested; self.manualSnapshotDirty = NO; KB_BUF_LOG(@"refreshFromProxy/accepted", harvested); } - (void)prepareSnapshotForDeleteWithContextBefore:(NSString *)before after:(NSString *)after { NSString *safeBefore = before ?: @""; NSString *safeAfter = after ?: @""; NSString *context = [safeBefore stringByAppendingString:safeAfter]; BOOL manualValid = (self.manualSnapshot.length > 0 && (context.length == 0 || (self.manualSnapshot.length >= context.length && [self.manualSnapshot rangeOfString:context].location != NSNotFound))); if (manualValid) { return; } if (self.liveText.length > 0) { self.manualSnapshot = self.liveText; self.manualSnapshotDirty = NO; KB_BUF_LOG(@"prepareSnapshotForDelete/fromLiveText", self.manualSnapshot); return; } if (context.length > 0) { self.manualSnapshot = context; self.manualSnapshotDirty = NO; KB_BUF_LOG(@"prepareSnapshotForDelete/fromContext", self.manualSnapshot); } } - (void)beginPendingClearSnapshot { if (self.pendingClearSnapshot.length > 0) { return; } if (self.manualSnapshot.length > 0) { self.pendingClearSnapshot = self.manualSnapshot; KB_BUF_LOG(@"beginPendingClearSnapshot/fromManual", self.pendingClearSnapshot); return; } if (self.liveText.length > 0) { self.pendingClearSnapshot = self.liveText; KB_BUF_LOG(@"beginPendingClearSnapshot/fromLive", self.pendingClearSnapshot); } } - (void)clearPendingClearSnapshot { self.pendingClearSnapshot = @""; } - (void)resetWithText:(NSString *)text { NSString *safe = text ?: @""; self.liveText = safe; self.manualSnapshot = safe; self.pendingClearSnapshot = @""; self.manualSnapshotDirty = NO; KB_BUF_LOG(@"resetWithText", safe); } - (void)appendText:(NSString *)text { if (text.length == 0) { return; } [self kb_syncManualSnapshotIfNeeded]; self.liveText = [self.liveText stringByAppendingString:text]; self.manualSnapshot = [self.manualSnapshot stringByAppendingString:text]; } - (void)deleteBackwardByCount:(NSUInteger)count { if (count == 0) { return; } self.liveText = [self kb_stringByDeletingComposedCharacters:count from:self.liveText]; self.manualSnapshot = [self kb_stringByDeletingComposedCharacters:count from:self.manualSnapshot]; } - (void)replaceTailWithText:(NSString *)text deleteCount:(NSUInteger)count { [self kb_syncManualSnapshotIfNeeded]; [self deleteBackwardByCount:count]; [self appendText:text]; } - (void)applyHoldDeleteCount:(NSUInteger)count { if (count == 0) { return; } self.liveText = [self kb_stringByDeletingComposedCharacters:count from:self.liveText]; self.manualSnapshotDirty = YES; } - (void)applyClearDeleteCount:(NSUInteger)count { if (count == 0) { return; } self.liveText = [self kb_stringByDeletingComposedCharacters:count from:self.liveText]; self.manualSnapshotDirty = YES; } - (void)clearAllLiveText { self.liveText = @""; self.pendingClearSnapshot = @""; self.manualSnapshotDirty = YES; } - (void)commitLiveToManual { self.manualSnapshot = self.liveText ?: @""; self.manualSnapshotDirty = NO; KB_BUF_LOG(@"commitLiveToManual", self.manualSnapshot); } - (void)restoreManualSnapshot { self.liveText = self.manualSnapshot ?: @""; } #pragma mark - Helpers - (void)kb_syncManualSnapshotIfNeeded { if (!self.manualSnapshotDirty) { return; } self.manualSnapshot = self.liveText ?: @""; self.manualSnapshotDirty = NO; } - (NSString *)kb_stringByDeletingComposedCharacters:(NSUInteger)count from:(NSString *)text { if (count == 0) { return text ?: @""; } NSString *source = text ?: @""; if (source.length == 0) { return @""; } __block NSUInteger removed = 0; __block NSUInteger endIndex = source.length; [source enumerateSubstringsInRange:NSMakeRange(0, source.length) options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse usingBlock:^(__unused NSString *substring, NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) { removed += 1; endIndex = substringRange.location; if (removed >= count) { *stop = YES; } }]; if (removed < count) { return @""; } return [source substringToIndex:endIndex]; } - (NSString *)kb_harvestFullTextFromProxy:(id)proxy { if (!proxy) { return @""; } if (![proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) { return @""; } static const NSInteger kKBHarvestMaxRounds = 160; static const NSInteger kKBHarvestMaxChars = 50000; NSInteger movedToEnd = 0; NSInteger movedLeft = 0; NSMutableArray *chunks = [NSMutableArray array]; NSInteger totalChars = 0; @try { NSInteger guard = 0; NSString *after = proxy.documentContextAfterInput ?: @""; while (after.length > 0 && guard < kKBHarvestMaxRounds) { NSInteger step = (NSInteger)after.length; [(id)proxy adjustTextPositionByCharacterOffset:step]; movedToEnd += step; guard += 1; after = proxy.documentContextAfterInput ?: @""; } guard = 0; NSString *before = proxy.documentContextBeforeInput ?: @""; while (before.length > 0 && guard < kKBHarvestMaxRounds && totalChars < kKBHarvestMaxChars) { [chunks addObject:before]; totalChars += (NSInteger)before.length; NSInteger step = (NSInteger)before.length; [(id)proxy adjustTextPositionByCharacterOffset:-step]; movedLeft += step; guard += 1; before = proxy.documentContextBeforeInput ?: @""; } } @finally { if (movedLeft != 0) { [(id)proxy adjustTextPositionByCharacterOffset:movedLeft]; } if (movedToEnd != 0) { [(id)proxy adjustTextPositionByCharacterOffset:-movedToEnd]; } } if (chunks.count == 0) { return @""; } NSMutableString *result = [NSMutableString stringWithCapacity:(NSUInteger)totalChars]; for (NSInteger i = (NSInteger)chunks.count - 1; i >= 0; i--) { NSString *part = chunks[(NSUInteger)i] ?: @""; if (part.length == 0) { continue; } [result appendString:part]; } return result; } @end