From 0e249c4346a31d266f8fce207ae01daf48dd066f Mon Sep 17 00:00:00 2001 From: Sean Murphy Date: Sun, 5 Apr 2026 15:38:19 -0400 Subject: [PATCH] fix(iOS): wrap dirty-range reapply in beginEditing/endEditing The nuke-and-recreate cycle in handleDirtyRangesStyling removes all attributes (including NSAttachmentAttributeName) then re-adds them. Without batching, each mutation triggers a separate layout pass. Between the remove and re-add, TextKit sees the object replacement character with no attachment and calculates zero glyph width, causing the caret to render inside inline images. Wrapping in beginEditing/endEditing coalesces the changes into a single layout pass so the transient zero-width state is never visible. --- ios/attributesManager/AttributesManager.mm | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ios/attributesManager/AttributesManager.mm b/ios/attributesManager/AttributesManager.mm index acdf16837..b2cfc1d8d 100644 --- a/ios/attributesManager/AttributesManager.mm +++ b/ios/attributesManager/AttributesManager.mm @@ -80,6 +80,12 @@ - (void)handleDirtyRangesStyling { presentStyles[@([[style class] getType])] = [style all:dirtyRange]; } + // Wrap the reset+reapply in beginEditing/endEditing so TextKit processes + // all attribute changes atomically. Without this, removing + // NSAttachmentAttributeName triggers a layout pass where \uFFFC has zero + // width, causing the caret to render inside inline images. + [_input->textView.textStorage beginEditing]; + // now reset the attributes to default ones [_input->textView.textStorage setAttributes:_input->defaultTypingAttributes range:dirtyRange]; @@ -110,6 +116,8 @@ - (void)handleDirtyRangesStyling { [style applyStyling:occurenceRange]; } } + + [_input->textView.textStorage endEditing]; } // do the typing attributes management, with no selection [self manageTypingAttributesWithOnlySelection:NO];