Skip to content

Commit

Permalink
Ensure ghost text doesn't appear when performing undos (#2105) (#2106)
Browse files Browse the repository at this point in the history
* Ensure ghost text doesn't appear when performing undos

* Clarify purpose of strict mode in `removingGhostTextFromString:strict:`

* Add clarifying comments about `removingGhostTextFromString:strict:` nil checks

---------

Co-authored-by: Adam Gleitman <adgleitm@microsoft.com>
  • Loading branch information
amgleitman and Adam Gleitman committed Apr 3, 2024
1 parent 5b8b8e6 commit 4ef8a1f
Showing 1 changed file with 57 additions and 24 deletions.
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

0 comments on commit 4ef8a1f

Please sign in to comment.