280 lines
9.8 KiB
Mathematica
280 lines
9.8 KiB
Mathematica
|
|
#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
|