修改键盘长按立即清空和撤销删除
This commit is contained in:
@@ -11,10 +11,10 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
- (instancetype)initWithContainerView:(UIView *)containerView;
|
||||
|
||||
/// 配置删除按钮(包含长按删除;可选是否显示“立刻清空”提示)
|
||||
/// 配置删除按钮(包含长按删除;可选是否显示“上滑清空”提示)
|
||||
- (void)bindDeleteButton:(nullable UIView *)button showClearLabel:(BOOL)showClearLabel;
|
||||
|
||||
/// 触发“立刻清空”逻辑(可用于功能面板的清空按钮)
|
||||
/// 触发“上滑清空”逻辑(可用于功能面板的清空按钮)
|
||||
- (void)performClearAction;
|
||||
|
||||
@end
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#import "KBResponderUtils.h"
|
||||
#import "KBSkinManager.h"
|
||||
#import "KBBackspaceUndoManager.h"
|
||||
#import "KBInputBufferManager.h"
|
||||
|
||||
static const NSTimeInterval kKBBackspaceLongPressMinDuration = 0.35;
|
||||
static const NSTimeInterval kKBBackspaceRepeatInterval = 0.06;
|
||||
@@ -20,11 +21,11 @@ static const CGFloat kKBBackspaceClearLabelHeight = 26.0;
|
||||
static const CGFloat kKBBackspaceClearLabelPaddingX = 10.0;
|
||||
static const CGFloat kKBBackspaceClearLabelTopGap = 6.0;
|
||||
static const CGFloat kKBBackspaceClearLabelHorizontalInset = 6.0;
|
||||
static const NSInteger kKBBackspaceClearBatchSize = 24;
|
||||
static const NSTimeInterval kKBBackspaceClearBatchInterval = 0.005;
|
||||
static const NSTimeInterval kKBBackspaceClearBatchInterval = 0.02;
|
||||
static const NSInteger kKBBackspaceClearMaxDeletes = 10000;
|
||||
static const NSInteger kKBBackspaceClearEmptyContextMaxRounds = 40;
|
||||
static const NSInteger kKBBackspaceClearMaxStep = 80;
|
||||
static const NSInteger kKBBackspaceClearDeletesPerTick = 10;
|
||||
|
||||
typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
KBBackspaceChunkClassUnknown = 0,
|
||||
@@ -34,6 +35,12 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
KBBackspaceChunkClassOther
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, KBClearPhase) {
|
||||
KBClearPhaseSkipWhitespace = 0,
|
||||
KBClearPhaseSkipTrailingBoundary,
|
||||
KBClearPhaseDeleteUntilBoundary
|
||||
};
|
||||
|
||||
@interface KBBackspaceLongPressHandler ()
|
||||
@property (nonatomic, weak) UIView *containerView;
|
||||
@property (nonatomic, weak) UIView *backspaceButton;
|
||||
@@ -50,6 +57,7 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
@property (nonatomic, strong) UILabel *backspaceClearLabel;
|
||||
@property (nonatomic, copy) NSString *pendingClearBefore;
|
||||
@property (nonatomic, copy) NSString *pendingClearAfter;
|
||||
@property (nonatomic, assign) KBClearPhase backspaceClearPhase;
|
||||
@end
|
||||
|
||||
@implementation KBBackspaceLongPressHandler
|
||||
@@ -57,6 +65,7 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
- (instancetype)initWithContainerView:(UIView *)containerView {
|
||||
if (self = [super init]) {
|
||||
_containerView = containerView;
|
||||
_backspaceClearPhase = KBClearPhaseSkipWhitespace;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -103,9 +112,17 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
}
|
||||
switch (gr.state) {
|
||||
case UIGestureRecognizerStateBegan: {
|
||||
[self kb_captureDeletionSnapshotIfNeeded];
|
||||
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
||||
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||
if (ivc) {
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
[[KBInputBufferManager shared] refreshFromProxyIfPossible:proxy];
|
||||
[[KBInputBufferManager shared] prepareSnapshotForDeleteWithContextBefore:proxy.documentContextBeforeInput
|
||||
after:proxy.documentContextAfterInput];
|
||||
}
|
||||
if (self.showClearLabelEnabled) {
|
||||
[self kb_capturePendingClearSnapshotIfNeeded];
|
||||
[[KBInputBufferManager shared] beginPendingClearSnapshot];
|
||||
}
|
||||
self.backspaceHoldToken += 1;
|
||||
NSUInteger token = self.backspaceHoldToken;
|
||||
@@ -141,6 +158,7 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
if (!ivc) { self.backspaceHoldActive = NO; return; }
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
||||
if (before.length == 0) { before = [KBInputBufferManager shared].liveText ?: @""; }
|
||||
NSTimeInterval elapsed = [NSDate date].timeIntervalSinceReferenceDate - self.backspaceHoldStartTime;
|
||||
NSInteger deleteCount = 1;
|
||||
if (before.length > 0) {
|
||||
@@ -152,9 +170,8 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
[self kb_showBackspaceClearLabelIfNeeded];
|
||||
}
|
||||
}
|
||||
for (NSInteger i = 0; i < deleteCount; i++) {
|
||||
[proxy deleteBackward];
|
||||
}
|
||||
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:(NSUInteger)deleteCount];
|
||||
[[KBInputBufferManager shared] applyHoldDeleteCount:(NSUInteger)deleteCount];
|
||||
|
||||
NSTimeInterval interval = [self kb_backspaceRepeatIntervalForElapsed:elapsed];
|
||||
__weak typeof(self) weakSelf = self;
|
||||
@@ -229,13 +246,16 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
|
||||
- (NSInteger)kb_clearDeleteCountForContext:(NSString *)context
|
||||
hitBoundary:(BOOL *)hitBoundary {
|
||||
if (context.length == 0) { return kKBBackspaceClearBatchSize; }
|
||||
if (context.length == 0) {
|
||||
if (hitBoundary) { *hitBoundary = NO; }
|
||||
return 1;
|
||||
}
|
||||
|
||||
static NSCharacterSet *sentenceBoundarySet = nil;
|
||||
static NSCharacterSet *whitespaceSet = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?;。!?;…\n"];
|
||||
sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?。!?"];
|
||||
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||||
});
|
||||
|
||||
@@ -310,6 +330,12 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
shouldClear = [self kb_isPointInsideBackspaceClearLabel:point];
|
||||
}
|
||||
}
|
||||
#if DEBUG
|
||||
NSLog(@"[kb_handleBackspaceLongPressEnded] shouldClear=%@ highlighted=%@ labelHidden=%@",
|
||||
shouldClear ? @"YES" : @"NO",
|
||||
self.backspaceClearHighlighted ? @"YES" : @"NO",
|
||||
self.backspaceClearLabel.hidden ? @"YES" : @"NO");
|
||||
#endif
|
||||
self.backspaceHoldActive = NO;
|
||||
self.backspaceChunkModeActive = NO;
|
||||
self.backspaceHoldToken += 1;
|
||||
@@ -320,6 +346,8 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
} else {
|
||||
self.pendingClearBefore = nil;
|
||||
self.pendingClearAfter = nil;
|
||||
[[KBInputBufferManager shared] clearPendingClearSnapshot];
|
||||
[[KBInputBufferManager shared] commitLiveToManual];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,7 +439,7 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
- (UILabel *)backspaceClearLabel {
|
||||
if (!_backspaceClearLabel) {
|
||||
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
|
||||
label.text = @"立刻清空";
|
||||
label.text = @"上滑清空";
|
||||
label.textAlignment = NSTextAlignmentCenter;
|
||||
label.font = [UIFont systemFontOfSize:12 weight:UIFontWeightSemibold];
|
||||
label.textColor = [KBSkinManager shared].current.keyTextColor ?: UIColor.blackColor;
|
||||
@@ -431,13 +459,14 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
||||
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||
if (ivc) {
|
||||
NSString *before = self.pendingClearBefore ?: (ivc.textDocumentProxy.documentContextBeforeInput ?: @"");
|
||||
NSString *after = self.pendingClearAfter ?: (ivc.textDocumentProxy.documentContextAfterInput ?: @"");
|
||||
[[KBBackspaceUndoManager shared] recordClearWithContextBefore:before after:after];
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
[[KBInputBufferManager shared] refreshFromProxyIfPossible:proxy];
|
||||
}
|
||||
self.pendingClearBefore = nil;
|
||||
self.pendingClearAfter = nil;
|
||||
[[KBInputBufferManager shared] clearPendingClearSnapshot];
|
||||
self.backspaceClearToken += 1;
|
||||
self.backspaceClearPhase = KBClearPhaseSkipWhitespace;
|
||||
NSUInteger token = self.backspaceClearToken;
|
||||
[self kb_clearAllInputStepForToken:token guard:0 emptyRounds:0];
|
||||
}
|
||||
@@ -450,40 +479,90 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||
if (!ivc) { return; }
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
||||
NSInteger count = before.length;
|
||||
NSInteger batch = 0;
|
||||
NSInteger nextEmptyRounds = emptyRounds;
|
||||
BOOL hitBoundary = NO;
|
||||
if (count > 0) {
|
||||
batch = [self kb_clearDeleteCountForContext:before hitBoundary:&hitBoundary];
|
||||
nextEmptyRounds = 0;
|
||||
} else {
|
||||
batch = kKBBackspaceClearBatchSize;
|
||||
nextEmptyRounds = emptyRounds + 1;
|
||||
}
|
||||
if (batch <= 0) { batch = 1; }
|
||||
static NSCharacterSet *sentenceBoundarySet = nil;
|
||||
static NSCharacterSet *whitespaceSet = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sentenceBoundarySet = [NSCharacterSet characterSetWithCharactersInString:@".!?。!?"];
|
||||
whitespaceSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||||
});
|
||||
KBClearPhase phase = self.backspaceClearPhase;
|
||||
|
||||
if (guard >= kKBBackspaceClearMaxDeletes ||
|
||||
nextEmptyRounds > kKBBackspaceClearEmptyContextMaxRounds) {
|
||||
NSInteger deletedThisTick = 0;
|
||||
BOOL shouldStop = NO;
|
||||
NSString *lastBefore = nil;
|
||||
for (NSInteger i = 0; i < kKBBackspaceClearDeletesPerTick; i++) {
|
||||
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
||||
if (before.length == 0) {
|
||||
nextEmptyRounds += 1;
|
||||
// 即使 context 为空,也尝试删一次(某些宿主延迟更新)
|
||||
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1];
|
||||
[[KBInputBufferManager shared] applyClearDeleteCount:1];
|
||||
deletedThisTick += 1;
|
||||
break;
|
||||
}
|
||||
nextEmptyRounds = 0;
|
||||
|
||||
if (lastBefore && [before isEqualToString:lastBefore] && deletedThisTick > 0) {
|
||||
// 宿主未及时刷新 context,留到下一 tick 再继续,避免越界/重复记录
|
||||
break;
|
||||
}
|
||||
lastBefore = before;
|
||||
|
||||
// 取最后一个组合字符
|
||||
__block NSString *lastChar = @"";
|
||||
[before enumerateSubstringsInRange:NSMakeRange(0, before.length)
|
||||
options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse
|
||||
usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) {
|
||||
lastChar = substring ?: @"";
|
||||
*stop = YES;
|
||||
}];
|
||||
if (lastChar.length == 0) { break; }
|
||||
|
||||
BOOL isWhitespace = ([lastChar rangeOfCharacterFromSet:whitespaceSet].location != NSNotFound);
|
||||
BOOL isBoundary = ([lastChar rangeOfCharacterFromSet:sentenceBoundarySet].location != NSNotFound);
|
||||
|
||||
if (phase == KBClearPhaseSkipWhitespace) {
|
||||
if (isWhitespace) {
|
||||
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1];
|
||||
[[KBInputBufferManager shared] applyClearDeleteCount:1];
|
||||
deletedThisTick += 1;
|
||||
continue;
|
||||
}
|
||||
phase = KBClearPhaseSkipTrailingBoundary;
|
||||
}
|
||||
|
||||
if (phase == KBClearPhaseSkipTrailingBoundary) {
|
||||
if (isBoundary) {
|
||||
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1];
|
||||
[[KBInputBufferManager shared] applyClearDeleteCount:1];
|
||||
deletedThisTick += 1;
|
||||
continue;
|
||||
}
|
||||
phase = KBClearPhaseDeleteUntilBoundary;
|
||||
}
|
||||
|
||||
// phase == DeleteUntilBoundary
|
||||
if (isBoundary) {
|
||||
shouldStop = YES; // 保留该句末符号
|
||||
break;
|
||||
}
|
||||
[[KBBackspaceUndoManager shared] captureAndDeleteBackwardFromProxy:proxy count:1];
|
||||
[[KBInputBufferManager shared] applyClearDeleteCount:1];
|
||||
deletedThisTick += 1;
|
||||
if (guard + deletedThisTick >= kKBBackspaceClearMaxDeletes) { break; }
|
||||
if (deletedThisTick >= kKBBackspaceClearMaxStep) { break; }
|
||||
}
|
||||
|
||||
self.backspaceClearPhase = phase;
|
||||
NSInteger nextGuard = guard + deletedThisTick;
|
||||
if (nextGuard >= kKBBackspaceClearMaxDeletes ||
|
||||
nextEmptyRounds > kKBBackspaceClearEmptyContextMaxRounds ||
|
||||
shouldStop) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (NSInteger i = 0; i < batch; i++) {
|
||||
[proxy deleteBackward];
|
||||
}
|
||||
|
||||
NSInteger nextGuard = guard + batch;
|
||||
BOOL shouldContinue = NO;
|
||||
if (count > 0 && !hitBoundary) {
|
||||
if (count > batch) {
|
||||
shouldContinue = YES;
|
||||
} else if ([proxy hasText]) {
|
||||
shouldContinue = YES;
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldContinue) { return; }
|
||||
__weak typeof(self) weakSelf = self;
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
|
||||
(int64_t)(kKBBackspaceClearBatchInterval * NSEC_PER_SEC)),
|
||||
@@ -513,7 +592,6 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
}
|
||||
|
||||
- (void)kb_capturePendingClearSnapshotIfNeeded {
|
||||
if ([KBBackspaceUndoManager shared].hasUndo) { return; }
|
||||
if (self.pendingClearBefore.length > 0 || self.pendingClearAfter.length > 0) { return; }
|
||||
UIResponder *start = (UIResponder *)([self kb_hostView] ?: self.backspaceButton);
|
||||
UIInputViewController *ivc = KBFindInputViewController(start);
|
||||
@@ -521,6 +599,10 @@ typedef NS_ENUM(NSInteger, KBBackspaceChunkClass) {
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
self.pendingClearBefore = proxy.documentContextBeforeInput ?: @"";
|
||||
self.pendingClearAfter = proxy.documentContextAfterInput ?: @"";
|
||||
#if DEBUG
|
||||
NSLog(@"[kb_capturePendingClearSnapshotIfNeeded/before] len=%lu text=%@", (unsigned long)self.pendingClearBefore.length, self.pendingClearBefore);
|
||||
NSLog(@"[kb_capturePendingClearSnapshotIfNeeded/after] len=%lu text=%@", (unsigned long)self.pendingClearAfter.length, self.pendingClearAfter);
|
||||
#endif
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -21,6 +21,9 @@ extern NSNotificationName const KBBackspaceUndoStateDidChangeNotification;
|
||||
/// 记录一次“立刻清空”删除的内容(基于 documentContextBeforeInput/AfterInput)。
|
||||
- (void)recordClearWithContextBefore:(NSString *)before after:(NSString *)after;
|
||||
|
||||
/// 记录本次将被 deleteBackward 的内容,并执行 deleteBackward(支持多次累计,撤销时一次性插回)。
|
||||
- (void)captureAndDeleteBackwardFromProxy:(id<UITextDocumentProxy>)proxy count:(NSUInteger)count;
|
||||
|
||||
/// 在指定 responder 处执行撤销(向光标处插回删除的内容)
|
||||
- (void)performUndoFromResponder:(UIResponder *)responder;
|
||||
|
||||
|
||||
@@ -5,13 +5,38 @@
|
||||
|
||||
#import "KBBackspaceUndoManager.h"
|
||||
#import "KBResponderUtils.h"
|
||||
#import "KBInputBufferManager.h"
|
||||
|
||||
NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspaceUndoStateDidChangeNotification";
|
||||
|
||||
#if DEBUG
|
||||
static NSString *KBLogString(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_UNDO_LOG(tag, text) NSLog(@"%@", KBLogString((tag), (text)))
|
||||
#else
|
||||
#define KB_UNDO_LOG(tag, text) do {} while(0)
|
||||
#endif
|
||||
|
||||
typedef NS_ENUM(NSInteger, KBUndoSnapshotSource) {
|
||||
KBUndoSnapshotSourceNone = 0,
|
||||
KBUndoSnapshotSourceDeletionSnapshot,
|
||||
KBUndoSnapshotSourceClear
|
||||
};
|
||||
|
||||
@interface KBBackspaceUndoManager ()
|
||||
@property (nonatomic, copy) NSString *undoText;
|
||||
@property (nonatomic, assign) NSInteger undoAfterLength;
|
||||
@property (nonatomic, assign) BOOL hasUndo;
|
||||
@property (nonatomic, assign) KBUndoSnapshotSource snapshotSource;
|
||||
@property (nonatomic, strong) NSMutableArray<NSString *> *undoDeletedPieces;
|
||||
@end
|
||||
|
||||
@implementation KBBackspaceUndoManager
|
||||
@@ -29,55 +54,189 @@ NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspa
|
||||
if (self = [super init]) {
|
||||
_undoText = @"";
|
||||
_undoAfterLength = 0;
|
||||
_snapshotSource = KBUndoSnapshotSourceNone;
|
||||
_undoDeletedPieces = [NSMutableArray array];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)captureAndDeleteBackwardFromProxy:(id<UITextDocumentProxy>)proxy count:(NSUInteger)count {
|
||||
if (!proxy || count == 0) { return; }
|
||||
|
||||
NSString *selected = proxy.selectedText ?: @"";
|
||||
NSString *ctxBefore = proxy.documentContextBeforeInput ?: @"";
|
||||
NSString *ctxAfter = proxy.documentContextAfterInput ?: @"";
|
||||
NSUInteger ctxLen = ctxBefore.length + ctxAfter.length;
|
||||
BOOL isSelectAllLike = (selected.length > 0 &&
|
||||
(ctxLen == 0 || selected.length >= MAX((NSUInteger)40, ctxLen * 2)));
|
||||
if (isSelectAllLike) {
|
||||
// “全选删除”在微信/QQ中通常拿不到可靠的全文,因此禁用撤销,避免插回错误/不完整内容。
|
||||
if (self.hasUndo) {
|
||||
[self registerNonClearAction];
|
||||
}
|
||||
#if DEBUG
|
||||
KB_UNDO_LOG(@"captureAndDelete/selectAllDisableUndo", selected);
|
||||
#endif
|
||||
[proxy deleteBackward];
|
||||
[[KBInputBufferManager shared] resetWithText:@""];
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self.hasUndo) {
|
||||
[self.undoDeletedPieces removeAllObjects];
|
||||
self.undoText = @"";
|
||||
self.undoAfterLength = 0;
|
||||
self.snapshotSource = KBUndoSnapshotSourceDeletionSnapshot;
|
||||
[self kb_updateHasUndo:YES];
|
||||
}
|
||||
|
||||
BOOL didAppend = NO;
|
||||
NSString *lastObservedBefore = nil;
|
||||
for (NSUInteger i = 0; i < count; i++) {
|
||||
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
||||
if (before.length > 0) {
|
||||
// 若宿主在同一 runloop 内不更新 context,则跳过记录,避免把同一个字符重复记录成“多句”。
|
||||
if (lastObservedBefore && [before isEqualToString:lastObservedBefore]) {
|
||||
// still delete, but don't record
|
||||
} else {
|
||||
NSString *piece = [self kb_lastComposedCharacterFromString:before];
|
||||
if (piece.length > 0) {
|
||||
[self.undoDeletedPieces addObject:piece];
|
||||
didAppend = YES;
|
||||
}
|
||||
lastObservedBefore = before;
|
||||
}
|
||||
}
|
||||
[proxy deleteBackward];
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
if (didAppend) {
|
||||
NSUInteger piecesCount = self.undoDeletedPieces.count;
|
||||
if (piecesCount <= 20) {
|
||||
KB_UNDO_LOG(@"captureAndDelete/undoInsertTextNow", [self kb_buildUndoInsertTextFromPieces]);
|
||||
} else if (piecesCount % 50 == 0) {
|
||||
NSString *lastPiece = self.undoDeletedPieces.lastObject ?: @"";
|
||||
NSLog(@"[captureAndDelete/undoPieces] pieces=%lu lastPiece=%@",
|
||||
(unsigned long)piecesCount,
|
||||
lastPiece);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)recordDeletionSnapshotBefore:(NSString *)before after:(NSString *)after {
|
||||
if (self.undoText.length > 0) { return; }
|
||||
if (self.hasUndo) { return; }
|
||||
NSString *pending = [KBInputBufferManager shared].pendingClearSnapshot;
|
||||
NSString *manual = [KBInputBufferManager shared].manualSnapshot;
|
||||
NSString *fallbackText = (pending.length > 0) ? pending : ((manual.length > 0) ? manual : [KBInputBufferManager shared].liveText);
|
||||
if (fallbackText.length > 0) {
|
||||
self.undoText = fallbackText;
|
||||
self.undoAfterLength = 0;
|
||||
self.snapshotSource = KBUndoSnapshotSourceDeletionSnapshot;
|
||||
KB_UNDO_LOG(@"recordDeletionSnapshot/fallback", self.undoText);
|
||||
[self kb_updateHasUndo:YES];
|
||||
return;
|
||||
}
|
||||
NSString *safeBefore = before ?: @"";
|
||||
NSString *safeAfter = after ?: @"";
|
||||
NSString *full = [safeBefore stringByAppendingString:safeAfter];
|
||||
if (full.length == 0) { return; }
|
||||
self.undoText = full;
|
||||
self.undoAfterLength = (NSInteger)safeAfter.length;
|
||||
self.snapshotSource = KBUndoSnapshotSourceDeletionSnapshot;
|
||||
KB_UNDO_LOG(@"recordDeletionSnapshot/context", self.undoText);
|
||||
[self kb_updateHasUndo:YES];
|
||||
}
|
||||
|
||||
- (void)recordClearWithContextBefore:(NSString *)before after:(NSString *)after {
|
||||
if (self.undoText.length == 0) {
|
||||
NSString *safeBefore = before ?: @"";
|
||||
NSString *safeAfter = after ?: @"";
|
||||
NSString *full = [safeBefore stringByAppendingString:safeAfter];
|
||||
if (full.length > 0) {
|
||||
self.undoText = full;
|
||||
self.undoAfterLength = (NSInteger)safeAfter.length;
|
||||
NSString *pending = [KBInputBufferManager shared].pendingClearSnapshot;
|
||||
NSString *manual = [KBInputBufferManager shared].manualSnapshot;
|
||||
NSString *fallbackText = (pending.length > 0) ? pending : ((manual.length > 0) ? manual : [KBInputBufferManager shared].liveText);
|
||||
|
||||
NSString *safeBefore = before ?: @"";
|
||||
NSString *safeAfter = after ?: @"";
|
||||
NSString *contextText = [[safeBefore stringByAppendingString:safeAfter] copy];
|
||||
|
||||
NSString *candidate = (fallbackText.length > 0) ? fallbackText : contextText;
|
||||
NSInteger candidateAfterLen = (fallbackText.length > 0) ? 0 : (NSInteger)safeAfter.length;
|
||||
|
||||
if (candidate.length == 0) { return; }
|
||||
|
||||
KB_UNDO_LOG(@"recordClear/candidate", candidate);
|
||||
|
||||
if (self.undoText.length > 0) {
|
||||
if (self.snapshotSource == KBUndoSnapshotSourceClear) {
|
||||
KB_UNDO_LOG(@"recordClear/ignored(alreadyClear)", self.undoText);
|
||||
[self kb_updateHasUndo:YES];
|
||||
return;
|
||||
}
|
||||
if (self.snapshotSource == KBUndoSnapshotSourceDeletionSnapshot) {
|
||||
if (candidate.length > self.undoText.length) {
|
||||
self.undoText = candidate;
|
||||
self.undoAfterLength = candidateAfterLen;
|
||||
KB_UNDO_LOG(@"recordClear/upgradedFromDeletion", self.undoText);
|
||||
} else {
|
||||
KB_UNDO_LOG(@"recordClear/keepDeletionSnapshot", self.undoText);
|
||||
}
|
||||
self.snapshotSource = KBUndoSnapshotSourceClear;
|
||||
[self kb_updateHasUndo:YES];
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (self.undoText.length == 0) { return; }
|
||||
|
||||
self.undoText = candidate;
|
||||
self.undoAfterLength = candidateAfterLen;
|
||||
self.snapshotSource = KBUndoSnapshotSourceClear;
|
||||
KB_UNDO_LOG(@"recordClear/set", self.undoText);
|
||||
[self kb_updateHasUndo:YES];
|
||||
}
|
||||
|
||||
- (void)performUndoFromResponder:(UIResponder *)responder {
|
||||
if (self.undoText.length == 0) { return; }
|
||||
if (!self.hasUndo) { return; }
|
||||
UIInputViewController *ivc = KBFindInputViewController(responder);
|
||||
if (!ivc) { return; }
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
[self kb_clearAllTextForProxy:proxy];
|
||||
[proxy insertText:self.undoText];
|
||||
if (self.undoAfterLength > 0 &&
|
||||
[proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) {
|
||||
[proxy adjustTextPositionByCharacterOffset:-self.undoAfterLength];
|
||||
NSString *curBefore = proxy.documentContextBeforeInput ?: @"";
|
||||
NSString *curAfter = proxy.documentContextAfterInput ?: @"";
|
||||
KB_UNDO_LOG(@"performUndo/currentBefore", curBefore);
|
||||
KB_UNDO_LOG(@"performUndo/currentAfter", curAfter);
|
||||
NSString *insertText = [self kb_buildUndoInsertTextFromPieces];
|
||||
if (insertText.length > 0) {
|
||||
KB_UNDO_LOG(@"performUndo/insertDeletedText", insertText);
|
||||
[proxy insertText:insertText];
|
||||
[[KBInputBufferManager shared] appendText:insertText];
|
||||
} else if (self.undoText.length > 0) {
|
||||
KB_UNDO_LOG(@"performUndo/fallbackUndoText", self.undoText);
|
||||
[self kb_clearAllTextForProxy:proxy];
|
||||
[proxy insertText:self.undoText];
|
||||
if (self.undoAfterLength > 0 &&
|
||||
[proxy respondsToSelector:@selector(adjustTextPositionByCharacterOffset:)]) {
|
||||
[proxy adjustTextPositionByCharacterOffset:-self.undoAfterLength];
|
||||
}
|
||||
[[KBInputBufferManager shared] resetWithText:self.undoText];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
self.undoText = @"";
|
||||
self.undoAfterLength = 0;
|
||||
self.snapshotSource = KBUndoSnapshotSourceNone;
|
||||
[self.undoDeletedPieces removeAllObjects];
|
||||
[self kb_updateHasUndo:NO];
|
||||
}
|
||||
|
||||
- (void)registerNonClearAction {
|
||||
if (self.undoText.length == 0) { return; }
|
||||
if (!self.hasUndo) { return; }
|
||||
if (self.undoText.length > 0) {
|
||||
KB_UNDO_LOG(@"registerNonClearAction/clearUndoText", self.undoText);
|
||||
}
|
||||
if (self.undoDeletedPieces.count > 0) {
|
||||
KB_UNDO_LOG(@"registerNonClearAction/clearDeletedPieces", [self kb_buildUndoInsertTextFromPieces]);
|
||||
}
|
||||
self.undoText = @"";
|
||||
self.undoAfterLength = 0;
|
||||
self.snapshotSource = KBUndoSnapshotSourceNone;
|
||||
[self.undoDeletedPieces removeAllObjects];
|
||||
[self kb_updateHasUndo:NO];
|
||||
}
|
||||
|
||||
@@ -89,6 +248,29 @@ NSNotificationName const KBBackspaceUndoStateDidChangeNotification = @"KBBackspa
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:KBBackspaceUndoStateDidChangeNotification object:self];
|
||||
}
|
||||
|
||||
- (NSString *)kb_lastComposedCharacterFromString:(NSString *)text {
|
||||
if (text.length == 0) { return @""; }
|
||||
__block NSString *last = @"";
|
||||
[text enumerateSubstringsInRange:NSMakeRange(0, text.length)
|
||||
options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationReverse
|
||||
usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) {
|
||||
last = substring ?: @"";
|
||||
*stop = YES;
|
||||
}];
|
||||
return last ?: @"";
|
||||
}
|
||||
|
||||
- (NSString *)kb_buildUndoInsertTextFromPieces {
|
||||
if (self.undoDeletedPieces.count == 0) { return @""; }
|
||||
NSMutableString *result = [NSMutableString string];
|
||||
for (NSInteger i = (NSInteger)self.undoDeletedPieces.count - 1; i >= 0; i--) {
|
||||
NSString *piece = self.undoDeletedPieces[(NSUInteger)i] ?: @"";
|
||||
if (piece.length == 0) { continue; }
|
||||
[result appendString:piece];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static const NSInteger kKBUndoClearMaxRounds = 200;
|
||||
|
||||
- (void)kb_clearAllTextForProxy:(id<UITextDocumentProxy>)proxy {
|
||||
|
||||
34
CustomKeyboard/Utils/KBInputBufferManager.h
Normal file
34
CustomKeyboard/Utils/KBInputBufferManager.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol UITextDocumentProxy;
|
||||
|
||||
@interface KBInputBufferManager : NSObject
|
||||
|
||||
+ (instancetype)shared;
|
||||
|
||||
@property (nonatomic, copy, readonly) NSString *liveText;
|
||||
@property (nonatomic, copy, readonly) NSString *manualSnapshot;
|
||||
@property (nonatomic, copy, readonly) NSString *pendingClearSnapshot;
|
||||
|
||||
- (void)seedIfEmptyWithContextBefore:(nullable NSString *)before after:(nullable NSString *)after;
|
||||
- (void)updateFromExternalContextBefore:(nullable NSString *)before after:(nullable NSString *)after;
|
||||
- (void)refreshFromProxyIfPossible:(nullable id<UITextDocumentProxy>)proxy;
|
||||
- (void)prepareSnapshotForDeleteWithContextBefore:(nullable NSString *)before
|
||||
after:(nullable NSString *)after;
|
||||
- (void)beginPendingClearSnapshot;
|
||||
- (void)clearPendingClearSnapshot;
|
||||
- (void)resetWithText:(NSString *)text;
|
||||
- (void)appendText:(NSString *)text;
|
||||
- (void)deleteBackwardByCount:(NSUInteger)count;
|
||||
- (void)replaceTailWithText:(NSString *)text deleteCount:(NSUInteger)count;
|
||||
- (void)applyHoldDeleteCount:(NSUInteger)count;
|
||||
- (void)applyClearDeleteCount:(NSUInteger)count;
|
||||
- (void)clearAllLiveText;
|
||||
- (void)commitLiveToManual;
|
||||
- (void)restoreManualSnapshot;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
279
CustomKeyboard/Utils/KBInputBufferManager.m
Normal file
279
CustomKeyboard/Utils/KBInputBufferManager.m
Normal file
@@ -0,0 +1,279 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user