Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure ghost text doesn't appear when performing undos #2105

Merged
merged 3 commits into from
Apr 3, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,16 @@ - (void)setAttributedText:(NSAttributedString *)attributedText
NSAttributedString *oldAttributedText = [self.backedTextInputView.attributedText copy];
NSInteger oldTextLength = oldAttributedText.string.length;

[self.backedTextInputView.undoManager registerUndoWithTarget:self handler:^(RCTBaseTextInputView *strongSelf) {
strongSelf.attributedText = oldAttributedText;
[strongSelf textInputDidChange];
}];
// Ghost text changes should not be part of the undo stack
if (!self.backedTextInputView.ghostTextChanging) {
// If there was ghost text previously, we don't want it showing up if we undo.
// If something goes wrong when trying to remove it, just stick with oldAttributedText.
NSAttributedString *oldAttributedTextWithoutGhostText = [self removingGhostTextFromString:oldAttributedText strict:YES] ?: oldAttributedText;
[self.backedTextInputView.undoManager registerUndoWithTarget:self handler:^(RCTBaseTextInputView *strongSelf) {
strongSelf.attributedText = oldAttributedTextWithoutGhostText;
[strongSelf textInputDidChange];
}];
}

self.backedTextInputView.attributedText = attributedText;

Expand Down Expand Up @@ -975,27 +981,12 @@ - (void)setGhostText:(NSString *)ghostText {
self.backedTextInputView.ghostTextChanging = YES;

if (_ghostText != nil) {
BOOL shouldDeleteGhostText = YES;
NSRange ghostTextRange = NSMakeRange(_ghostTextPosition, _ghostText.length);
NSMutableAttributedString *attributedString = [self.attributedText mutableCopy];

if ([attributedString length] < NSMaxRange(ghostTextRange)) {
RCTAssert(false, @"Ghost text not fully present in text view text");
shouldDeleteGhostText = NO;
}

NSString *actualGhostText = shouldDeleteGhostText
? [[attributedString attributedSubstringFromRange:ghostTextRange] string]
: nil;
// When setGhostText: is called after making a standard edit, the ghost text may already be gone
BOOL ghostTextMayAlreadyBeGone = newGhostText == nil;
NSAttributedString *attributedStringWithoutGhostText = [self removingGhostTextFromString:self.attributedText strict:!ghostTextMayAlreadyBeGone];

if (![actualGhostText isEqual:_ghostText]) {
RCTAssert(false, @"Ghost text does not match text view text");
shouldDeleteGhostText = NO;
}

if (shouldDeleteGhostText) {
[attributedString deleteCharactersInRange:ghostTextRange];
self.attributedText = attributedString;
if (attributedStringWithoutGhostText != nil) {
self.attributedText = attributedStringWithoutGhostText;
[self setSelectionStart:selection.start selectionEnd:selection.end];
}
}
Expand All @@ -1016,6 +1007,48 @@ - (void)setGhostText:(NSString *)ghostText {
self.backedTextInputView.ghostTextChanging = NO;
}

/**
* Attempts to remove the ghost text from a provided string given our current state.
*
* If `strict` mode is enabled, this method assumes the ghost text exists exactly
* where we expect it to be. We assert and return `nil` if we don't find the expected ghost text.
* It's the responsibility of the caller to make sure the result isn't `nil`.
*
* If disabled, we allow for the possibility that the ghost text has already been removed,
* which can happen if a delegate callback is trying to remove ghost text after invoking `setAttributedText:`.
*/
- (NSAttributedString *)removingGhostTextFromString:(NSAttributedString *)string strict:(BOOL)strict {
if (_ghostText == nil) {
return string;
}

NSRange ghostTextRange = NSMakeRange(_ghostTextPosition, _ghostText.length);
NSMutableAttributedString *attributedString = [string mutableCopy];

if ([attributedString length] < NSMaxRange(ghostTextRange)) {
if (strict) {
RCTAssert(false, @"Ghost text not fully present in text view text");
return nil;
} else {
return string;
}
}

NSString *actualGhostText = [[attributedString attributedSubstringFromRange:ghostTextRange] string];

if (![actualGhostText isEqual:_ghostText]) {
if (strict) {
RCTAssert(false, @"Ghost text does not match text view text");
return nil;
} else {
return string;
}
}

[attributedString deleteCharactersInRange:ghostTextRange];
return attributedString;
}

// macOS]

#pragma mark - Helpers
Expand Down
Loading