280 lines
9.8 KiB
Objective-C
280 lines
9.8 KiB
Objective-C
#import "KBInputBufferManager.h"
|
||
#import <UIKit/UIKit.h>
|
||
|
||
#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<UITextDocumentProxy>)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<UITextDocumentProxy>)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<NSString *> *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
|