From b2de8e21c59cb0a2a944e48afa8227e572a20f9c Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Tue, 7 Oct 2025 11:03:34 +0200 Subject: [PATCH 1/3] fix: unify the behavior around backspacing lists and quotes --- ios/EnrichedTextInputView.mm | 10 ++++---- ios/styles/BlockQuoteStyle.mm | 35 +++++++++------------------ ios/styles/OrderedListStyle.mm | 35 +++++++++------------------ ios/styles/UnorderedListStyle.mm | 35 +++++++++------------------ ios/utils/ParagraphAttributesUtils.mm | 13 +++++++++- 5 files changed, 53 insertions(+), 75 deletions(-) diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 23178d5ef..445488690 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -980,8 +980,8 @@ - (void)manageSelectionBasedChanges { } // typing attributes for empty lines selection reset - NSString *currentString = [[textView.textStorage.string copy] stringByReplacingOccurrencesOfString:@"\u200B" withString:@""]; - if(textView.selectedRange.length == 0 && [_recentlyEmittedString isEqualToString: currentString] ) { + NSString *currentString = [textView.textStorage.string copy]; + if(textView.selectedRange.length == 0 && [_recentlyEmittedString isEqualToString:currentString]) { // no string change means only a selection changed with no character changes NSRange paragraphRange = [textView.textStorage.string paragraphRangeForRange:textView.selectedRange]; if( @@ -1012,15 +1012,15 @@ - (void)anyTextMayHaveBeenModified { return; } + // zero width space adding or removal + [ZeroWidthSpaceUtils handleZeroWidthSpacesInInput:self]; + // emptying input typing attributes management if(textView.textStorage.string.length == 0 && _recentlyEmittedString.length > 0) { // reset typing attribtues textView.typingAttributes = defaultTypingAttributes; } - // zero width space removal - [ZeroWidthSpaceUtils handleZeroWidthSpacesInInput:self]; - // inline code on newlines fix InlineCodeStyle *codeStyle = stylesDict[@([InlineCodeStyle getStyleType])]; if(codeStyle != nullptr) { diff --git a/ios/styles/BlockQuoteStyle.mm b/ios/styles/BlockQuoteStyle.mm index 71ade37ed..4c04cbb12 100644 --- a/ios/styles/BlockQuoteStyle.mm +++ b/ios/styles/BlockQuoteStyle.mm @@ -119,30 +119,19 @@ - (void)removeTypingAttributes { } - (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text { - if( - [self detectStyle:_input->textView.selectedRange] && - NSEqualRanges(_input->textView.selectedRange, NSMakeRange(0, 0)) && - [text isEqualToString:@""] - ) { - // removing first quote line by backspacing doesn't remove typing attributes because it doesn't run textViewDidChange - // so we try guessing that a line should be deleted here + if([self detectStyle:_input->textView.selectedRange] && text.length == 0) { + // backspace while the style is active + NSRange paragraphRange = [_input->textView.textStorage.string paragraphRangeForRange:_input->textView.selectedRange]; - [self removeAttributes:paragraphRange]; - return YES; - } else if( - [self detectStyle:_input->textView.selectedRange] && - [text isEqualToString:@""] - ) { - // other case; make sure removing all the (non newline) text from a quto line also removes the line itself - NSRange paragraphRange = [_input->textView.textStorage.string paragraphRangeForRange:range]; - NSValue *nonNewlineVal = [ParagraphsUtils getNonNewlineRangesIn:_input->textView range:paragraphRange].firstObject; - if(nonNewlineVal == nullptr) { - return NO; - } - NSRange nonNewlineRange = [nonNewlineVal rangeValue]; - if(NSEqualRanges(range, nonNewlineRange)) { - [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:_input withSelection:YES]; - [self removeAttributes:NSMakeRange(range.location, 0)]; + + if(NSEqualRanges(_input->textView.selectedRange, NSMakeRange(0, 0))) { + // a backspace on the very first input's line quote + // it doesn't run textVieDidChange so we need to manually remove attributes + [self removeAttributes:paragraphRange]; + return YES; + } else if(range.location == paragraphRange.location - 1) { + // same case in other lines; here, the removed range location will be exactly 1 less than paragraph range location + [self removeAttributes:paragraphRange]; return YES; } } diff --git a/ios/styles/OrderedListStyle.mm b/ios/styles/OrderedListStyle.mm index e4f9a5f5f..472118dfd 100644 --- a/ios/styles/OrderedListStyle.mm +++ b/ios/styles/OrderedListStyle.mm @@ -131,30 +131,19 @@ - (void)removeTypingAttributes { } - (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text { - if( - [self detectStyle:_input->textView.selectedRange] && - NSEqualRanges(_input->textView.selectedRange, NSMakeRange(0, 0)) && - [text isEqualToString:@""] - ) { - // removing first list point by backspacing doesn't remove typing attributes because it doesn't run textViewDidChange - // so we try guessing that a point should be deleted here + if([self detectStyle:_input->textView.selectedRange] && text.length == 0) { + // backspace while the style is active + NSRange paragraphRange = [_input->textView.textStorage.string paragraphRangeForRange:_input->textView.selectedRange]; - [self removeAttributes:paragraphRange]; - return YES; - } else if( - [self detectStyle:_input->textView.selectedRange] && - [text isEqualToString:@""] - ) { - // other case; make sure removing all the (non newline) text from a list item also removes the item itself - NSRange paragraphRange = [_input->textView.textStorage.string paragraphRangeForRange:range]; - NSValue *nonNewlineVal = [ParagraphsUtils getNonNewlineRangesIn:_input->textView range:paragraphRange].firstObject; - if(nonNewlineVal == nullptr) { - return NO; - } - NSRange nonNewlineRange = [nonNewlineVal rangeValue]; - if(NSEqualRanges(range, nonNewlineRange)) { - [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:_input withSelection:YES]; - [self removeAttributes:NSMakeRange(range.location, 0)]; + + if(NSEqualRanges(_input->textView.selectedRange, NSMakeRange(0, 0))) { + // a backspace on the very first input's line list point + // it doesn't run textVieDidChange so we need to manually remove attributes + [self removeAttributes:paragraphRange]; + return YES; + } else if(range.location == paragraphRange.location - 1) { + // same case in other lines; here, the removed range location will be exactly 1 less than paragraph range location + [self removeAttributes:paragraphRange]; return YES; } } diff --git a/ios/styles/UnorderedListStyle.mm b/ios/styles/UnorderedListStyle.mm index 95270ca51..e161fc2d6 100644 --- a/ios/styles/UnorderedListStyle.mm +++ b/ios/styles/UnorderedListStyle.mm @@ -131,30 +131,19 @@ - (void)removeTypingAttributes { } - (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text { - if( - [self detectStyle:_input->textView.selectedRange] && - NSEqualRanges(_input->textView.selectedRange, NSMakeRange(0, 0)) && - [text isEqualToString:@""] - ) { - // removing first list point by backspacing doesn't remove typing attributes because it doesn't run textViewDidChange - // so we try guessing that a point should be deleted here + if([self detectStyle:_input->textView.selectedRange] && text.length == 0) { + // backspace while the style is active + NSRange paragraphRange = [_input->textView.textStorage.string paragraphRangeForRange:_input->textView.selectedRange]; - [self removeAttributes:paragraphRange]; - return YES; - } else if( - [self detectStyle:_input->textView.selectedRange] && - [text isEqualToString:@""] - ) { - // other case; make sure removing all the (non newline) text from a list item also removes the item itself - NSRange paragraphRange = [_input->textView.textStorage.string paragraphRangeForRange:range]; - NSValue *nonNewlineVal = [ParagraphsUtils getNonNewlineRangesIn:_input->textView range:paragraphRange].firstObject; - if(nonNewlineVal == nullptr) { - return NO; - } - NSRange nonNewlineRange = [nonNewlineVal rangeValue]; - if(NSEqualRanges(range, nonNewlineRange)) { - [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:_input withSelection:YES]; - [self removeAttributes:NSMakeRange(range.location, 0)]; + + if(NSEqualRanges(_input->textView.selectedRange, NSMakeRange(0, 0))) { + // a backspace on the very first input's line list point + // it doesn't run textVieDidChange so we need to manually remove attributes + [self removeAttributes:paragraphRange]; + return YES; + } else if(range.location == paragraphRange.location - 1) { + // same case in other lines; here, the removed range location will be exactly 1 less than paragraph range location + [self removeAttributes:paragraphRange]; return YES; } } diff --git a/ios/utils/ParagraphAttributesUtils.mm b/ios/utils/ParagraphAttributesUtils.mm index 38b69c825..aeedde07e 100644 --- a/ios/utils/ParagraphAttributesUtils.mm +++ b/ios/utils/ParagraphAttributesUtils.mm @@ -1,5 +1,6 @@ #import "ParagraphAttributesUtils.h" #import "EnrichedTextInputView.h" +#import "StyleHeaders.h" #import "ParagraphsUtils.h" #import "TextInsertionUtils.h" @@ -10,6 +11,10 @@ @implementation ParagraphAttributesUtils // hence the solution - reset typing attributes + (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text input:(id)input { EnrichedTextInputView *typedInput = (EnrichedTextInputView *)input; + UnorderedListStyle *ulStyle = typedInput->stylesDict[@([UnorderedListStyle getStyleType])]; + OrderedListStyle *olStyle = typedInput->stylesDict[@([OrderedListStyle getStyleType])]; + BlockQuoteStyle *bqStyle = typedInput->stylesDict[@([BlockQuoteStyle getStyleType])]; + if(typedInput == nullptr) { return NO; } @@ -21,6 +26,12 @@ + (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text i // find a non-newline range of the paragraph NSRange paragraphRange = [typedInput->textView.textStorage.string paragraphRangeForRange:range]; + + // for lists and quotes we don't want that behavior; we want zero width spaces to appear there + if([ulStyle detectStyle:paragraphRange] || [olStyle detectStyle:paragraphRange] || [bqStyle detectStyle:paragraphRange]) { + return NO; + } + NSArray *paragraphs = [ParagraphsUtils getNonNewlineRangesIn:typedInput->textView range:paragraphRange]; if(paragraphs.count == 0) { return NO; @@ -28,7 +39,7 @@ + (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text i NSRange nonNewlineRange = [(NSValue *)paragraphs.firstObject rangeValue]; - // if the backspace removes the whole content of a paragraph - do the thing + // if the backspace removes the whole content of a paragraph, we remove the typing attributes if(NSEqualRanges(nonNewlineRange, range)) { // do the replacement manually [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:typedInput withSelection:YES]; From 3034f8dc890bf6da522de85b6105ed8c17268cd7 Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Tue, 7 Oct 2025 12:02:16 +0200 Subject: [PATCH 2/3] fix: one more strange edge case --- ios/utils/OccurenceUtils.mm | 2 +- ios/utils/ParagraphAttributesUtils.mm | 41 +++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/ios/utils/OccurenceUtils.mm b/ios/utils/OccurenceUtils.mm index 7c5b707bb..796282eb4 100644 --- a/ios/utils/OccurenceUtils.mm +++ b/ios/utils/OccurenceUtils.mm @@ -47,7 +47,7 @@ + (BOOL)detect NSRange attrRange = NSMakeRange(0, 0); attrValue = [input->textView.textStorage attribute:key atIndex:index effectiveRange:&attrRange]; } - return condition(attrValue, NSMakeRange(index, 0)); + return condition(attrValue, detectionRange); } + (BOOL)detectMultiple diff --git a/ios/utils/ParagraphAttributesUtils.mm b/ios/utils/ParagraphAttributesUtils.mm index aeedde07e..bc7f1b303 100644 --- a/ios/utils/ParagraphAttributesUtils.mm +++ b/ios/utils/ParagraphAttributesUtils.mm @@ -27,11 +27,6 @@ + (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text i // find a non-newline range of the paragraph NSRange paragraphRange = [typedInput->textView.textStorage.string paragraphRangeForRange:range]; - // for lists and quotes we don't want that behavior; we want zero width spaces to appear there - if([ulStyle detectStyle:paragraphRange] || [olStyle detectStyle:paragraphRange] || [bqStyle detectStyle:paragraphRange]) { - return NO; - } - NSArray *paragraphs = [ParagraphsUtils getNonNewlineRangesIn:typedInput->textView range:paragraphRange]; if(paragraphs.count == 0) { return NO; @@ -41,6 +36,42 @@ + (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text i // if the backspace removes the whole content of a paragraph, we remove the typing attributes if(NSEqualRanges(nonNewlineRange, range)) { + + // edge case with lists and blockquotes: + // for some reason, removing all characters from a list point that is: both the first point of a list and is the last thing in the input, doesn't preserve typing attributes + // so we manually remove the characters and reapply attribtues for zero width space to appear there + // otherwise (there still is list or quote) we don't want no actions because attributes are properly preserved and zero width space appears + if([ulStyle detectStyle:paragraphRange]) { + if(NSMaxRange(nonNewlineRange) == typedInput->textView.textStorage.string.length) { + // manually remove the characters + [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:typedInput withSelection:YES]; + // add atributes again + [ulStyle addAttributes:NSMakeRange(range.location, 0)]; + return YES; + } + return NO; + } + if([olStyle detectStyle:paragraphRange]) { + if(NSMaxRange(nonNewlineRange) == typedInput->textView.textStorage.string.length) { + // manually remove the characters + [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:typedInput withSelection:YES]; + // add atributes again + [olStyle addAttributes:NSMakeRange(range.location, 0)]; + return YES; + } + return NO; + } + if([bqStyle detectStyle:paragraphRange]) { + if(NSMaxRange(nonNewlineRange) == typedInput->textView.textStorage.string.length) { + // manually remove the characters + [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:typedInput withSelection:YES]; + // add atributes again + [bqStyle addAttributes:NSMakeRange(range.location, 0)]; + return YES; + } + return NO; + } + // do the replacement manually [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:typedInput withSelection:YES]; // reset typing attribtues From 3ae7cb04d46500ade92a89c84ba0113928fbd0d6 Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Tue, 7 Oct 2025 12:16:29 +0200 Subject: [PATCH 3/3] fix: one more clarification/improvement --- ios/utils/ParagraphAttributesUtils.mm | 50 +++++++++------------------ 1 file changed, 16 insertions(+), 34 deletions(-) diff --git a/ios/utils/ParagraphAttributesUtils.mm b/ios/utils/ParagraphAttributesUtils.mm index bc7f1b303..4cf63840d 100644 --- a/ios/utils/ParagraphAttributesUtils.mm +++ b/ios/utils/ParagraphAttributesUtils.mm @@ -34,42 +34,24 @@ + (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text i NSRange nonNewlineRange = [(NSValue *)paragraphs.firstObject rangeValue]; - // if the backspace removes the whole content of a paragraph, we remove the typing attributes - if(NSEqualRanges(nonNewlineRange, range)) { - - // edge case with lists and blockquotes: - // for some reason, removing all characters from a list point that is: both the first point of a list and is the last thing in the input, doesn't preserve typing attributes - // so we manually remove the characters and reapply attribtues for zero width space to appear there - // otherwise (there still is list or quote) we don't want no actions because attributes are properly preserved and zero width space appears - if([ulStyle detectStyle:paragraphRange]) { - if(NSMaxRange(nonNewlineRange) == typedInput->textView.textStorage.string.length) { - // manually remove the characters - [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:typedInput withSelection:YES]; - // add atributes again - [ulStyle addAttributes:NSMakeRange(range.location, 0)]; - return YES; - } - return NO; + // if the backspace removes the whole content of a paragraph (possibly more but has to start where the paragraph starts), we remove the typing attributes + if(range.location == nonNewlineRange.location && range.length >= nonNewlineRange.length) { + // for lists and quotes we want to remove the characters but keep attribtues so that a zero width space appears here + // so we do the removing manually and reapply attributes + if([ulStyle detectStyle:nonNewlineRange]) { + [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:typedInput withSelection:YES]; + [ulStyle addAttributes:NSMakeRange(range.location, 0)]; + return YES; } - if([olStyle detectStyle:paragraphRange]) { - if(NSMaxRange(nonNewlineRange) == typedInput->textView.textStorage.string.length) { - // manually remove the characters - [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:typedInput withSelection:YES]; - // add atributes again - [olStyle addAttributes:NSMakeRange(range.location, 0)]; - return YES; - } - return NO; + if([olStyle detectStyle:nonNewlineRange]) { + [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:typedInput withSelection:YES]; + [olStyle addAttributes:NSMakeRange(range.location, 0)]; + return YES; } - if([bqStyle detectStyle:paragraphRange]) { - if(NSMaxRange(nonNewlineRange) == typedInput->textView.textStorage.string.length) { - // manually remove the characters - [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:typedInput withSelection:YES]; - // add atributes again - [bqStyle addAttributes:NSMakeRange(range.location, 0)]; - return YES; - } - return NO; + if([bqStyle detectStyle:nonNewlineRange]) { + [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:typedInput withSelection:YES]; + [bqStyle addAttributes:NSMakeRange(range.location, 0)]; + return YES; } // do the replacement manually