Files
keyboard/CustomKeyboard/Utils/KBInputBufferManager.m

280 lines
9.8 KiB
Objective-C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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