diff --git a/attributed_text/lib/src/attributed_text.dart b/attributed_text/lib/src/attributed_text.dart index a554282aa..b6dfc4289 100644 --- a/attributed_text/lib/src/attributed_text.dart +++ b/attributed_text/lib/src/attributed_text.dart @@ -421,16 +421,20 @@ class AttributedText { final textEndCopyOffset = (endOffset ?? length) - placeholdersBeforeStartOffset.length - placeholdersAfterStartBeforeEndOffset.length; + // The span marker offsets are based on the text with placeholders, so we need + // to copy the text with placeholders to ensure the span markers are correct. + final textWithPlaceholders = toPlainText(); + // Note: -1 because copyText() uses an exclusive `start` and `end` but // _copyAttributionRegion() uses an inclusive `start` and `end`. - final startCopyOffset = startOffset < _text.length ? startOffset : _text.length - 1; + final startCopyOffset = startOffset < textWithPlaceholders.length ? startOffset : textWithPlaceholders.length - 1; int endCopyOffset; if (endOffset == startOffset) { endCopyOffset = startCopyOffset; } else if (endOffset != null) { endCopyOffset = endOffset - 1; } else { - endCopyOffset = _text.length - 1; + endCopyOffset = textWithPlaceholders.length - 1; } _log.fine('offsets, start: $startCopyOffset, end: $endCopyOffset'); diff --git a/attributed_text/test/attributed_text_placeholders_test.dart b/attributed_text/test/attributed_text_placeholders_test.dart index 3022a0564..1f0b283b2 100644 --- a/attributed_text/test/attributed_text_placeholders_test.dart +++ b/attributed_text/test/attributed_text_placeholders_test.dart @@ -440,6 +440,46 @@ void main() { }), ); }); + + test("empty text with leading placeholder (with attributions)", () { + // Create an empty text containing a placeholder with an attribution around it. + final text = AttributedText( + '', + AttributedSpans( + attributions: [ + const SpanMarker( + attribution: _bold, + offset: 0, + markerType: SpanMarkerType.start, + ), + const SpanMarker( + attribution: _bold, + offset: 0, + markerType: SpanMarkerType.end, + ), + ], + ), + { + 0: const _FakePlaceholder('leading'), + }, + ); + + expect( + text.copyText(0), + AttributedText( + "", + AttributedSpans( + attributions: const [ + SpanMarker(attribution: _bold, offset: 0, markerType: SpanMarkerType.start), + SpanMarker(attribution: _bold, offset: 0, markerType: SpanMarkerType.end), + ], + ), + { + 0: const _FakePlaceholder("leading"), + }, + ), + ); + }); }); group("copy and append >", () { diff --git a/super_editor/test/super_editor/text_entry/inline_widgets_test.dart b/super_editor/test/super_editor/text_entry/inline_widgets_test.dart index 1bed3a4fa..a6afd5821 100644 --- a/super_editor/test/super_editor/text_entry/inline_widgets_test.dart +++ b/super_editor/test/super_editor/text_entry/inline_widgets_test.dart @@ -133,6 +133,58 @@ void main() { ), ); }); + + testWidgetsOnArbitraryDesktop("can insert an inline widget with attributions in an empty paragraph", + (tester) async { + final editor = await _pumpScaffold(tester); + + // Insert an empty text containing a placeholder with an attribution around it. + editor.execute([ + InsertStyledTextAtCaretRequest( + AttributedText( + '', + AttributedSpans( + attributions: [ + const SpanMarker( + attribution: _emojiAttribution, + offset: 0, + markerType: SpanMarkerType.start, + ), + const SpanMarker( + attribution: _emojiAttribution, + offset: 0, + markerType: SpanMarkerType.end, + ), + ], + ), + { + 0: const _TestPlaceholder(), + }), + ), + ]); + await tester.pump(); + + // Ensure we can type after the inline placeholder was added. + await tester.typeImeText('hello'); + + // Ensure the inline widget was kept, the text was inserted, and the attribution + // was not applied to the inserted characters. + expect( + SuperEditorInspector.findTextInComponent('1'), + AttributedText( + 'hello', + AttributedSpans( + attributions: const [ + SpanMarker(attribution: _emojiAttribution, offset: 0, markerType: SpanMarkerType.start), + SpanMarker(attribution: _emojiAttribution, offset: 0, markerType: SpanMarkerType.end), + ], + ), + { + 0: const _TestPlaceholder(), + }, + ), + ); + }); }); } @@ -165,3 +217,5 @@ Widget? _buildInlineTestWidget(BuildContext context, TextStyle style, Object pla class _TestPlaceholder { const _TestPlaceholder(); } + +const _emojiAttribution = NamedAttribution('emoji');