Skip to content

Commit 75309e6

Browse files
[SuperEditor] Fix text duplicated when a placeholder is inserted in a text with attributions (Resolves #2595)
1 parent 068aeb0 commit 75309e6

File tree

2 files changed

+133
-3
lines changed

2 files changed

+133
-3
lines changed

Diff for: super_editor/lib/src/infrastructure/attributed_text_styles.dart

+16-3
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,26 @@ extension ComputeTextSpan on AttributedText {
6363
}
6464
} else {
6565
// This section is text. The end of this text is either the
66-
// end of the AttributedText, or the index of the next placeholder.
66+
// end of the current span, or the index of the next placeholder.
6767
contentEnd = span.end + 1;
68+
69+
final nextSpan = spanIndex + 1 < collapsedSpans.length //
70+
? collapsedSpans[spanIndex + 1]
71+
: null;
6872
for (final entry in placeholders.entries) {
69-
if (entry.key > start) {
70-
contentEnd = entry.key;
73+
if (entry.key <= start) {
74+
// This placeholder is before the current span.
75+
continue;
76+
}
77+
78+
if (nextSpan != null && entry.key >= nextSpan.start) {
79+
// This placeholder is beyond the next span.
7180
break;
7281
}
82+
83+
// This placeholder is within the current span.
84+
contentEnd = entry.key;
85+
break;
7386
}
7487

7588
inlineSpans.add(
+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
import 'package:super_editor/super_editor.dart';
4+
5+
void main() {
6+
group('SuperEditor > computeInlineSpan >', () {
7+
testWidgets('does not modify text with attributions and a placeholder at the beginning', (tester) async {
8+
// Pump a widget because we need a BuildContext to compute the InlineSpan.
9+
await tester.pumpWidget(
10+
const MaterialApp(),
11+
);
12+
13+
// Create an AttributedText with the words "Welcome" and "SuperEditor" in bold and with a leading placeholder.
14+
final text = AttributedText(
15+
'Welcome to SuperEditor',
16+
AttributedSpans(
17+
attributions: [
18+
const SpanMarker(attribution: boldAttribution, offset: 0, markerType: SpanMarkerType.start),
19+
const SpanMarker(attribution: boldAttribution, offset: 6, markerType: SpanMarkerType.end),
20+
const SpanMarker(attribution: boldAttribution, offset: 11, markerType: SpanMarkerType.start),
21+
const SpanMarker(attribution: boldAttribution, offset: 21, markerType: SpanMarkerType.end),
22+
],
23+
),
24+
{0: const _ExamplePlaceholder()},
25+
);
26+
27+
final inlineSpan = text.computeInlineSpan(
28+
find.byType(MaterialApp).evaluate().first as BuildContext,
29+
defaultStyleBuilder,
30+
[_inlineWidgetBuilder],
31+
);
32+
33+
// Ensure the text was not modified.
34+
expect(
35+
inlineSpan.toPlainText(includePlaceholders: false),
36+
'Welcome to SuperEditor',
37+
);
38+
});
39+
40+
testWidgets('does not modify text with attributions and a placeholder at the middle', (tester) async {
41+
// Pump a widget because we need a BuildContext to compute the InlineSpan.
42+
await tester.pumpWidget(
43+
const MaterialApp(),
44+
);
45+
46+
// Create an AttributedText with the words "Welcome" and "SuperEditor" in bold and with a
47+
// placeholder after the word "to".
48+
final text = AttributedText(
49+
'Welcome to SuperEditor',
50+
AttributedSpans(
51+
attributions: [
52+
const SpanMarker(attribution: boldAttribution, offset: 0, markerType: SpanMarkerType.start),
53+
const SpanMarker(attribution: boldAttribution, offset: 6, markerType: SpanMarkerType.end),
54+
const SpanMarker(attribution: boldAttribution, offset: 11, markerType: SpanMarkerType.start),
55+
const SpanMarker(attribution: boldAttribution, offset: 21, markerType: SpanMarkerType.end),
56+
],
57+
),
58+
{10: const _ExamplePlaceholder()},
59+
);
60+
61+
final inlineSpan = text.computeInlineSpan(
62+
find.byType(MaterialApp).evaluate().first as BuildContext,
63+
defaultStyleBuilder,
64+
[_inlineWidgetBuilder],
65+
);
66+
67+
// Ensure the text was not modified.
68+
expect(
69+
inlineSpan.toPlainText(includePlaceholders: false),
70+
'Welcome to SuperEditor',
71+
);
72+
});
73+
74+
testWidgets('does not modify text with attributions and a placeholder at the end', (tester) async {
75+
// Pump a widget because we need a BuildContext to compute the InlineSpan.
76+
await tester.pumpWidget(
77+
const MaterialApp(),
78+
);
79+
80+
// Create an AttributedText with the word "Welcome" in bold and with a trailing placeholder.
81+
// Create an AttributedText with the words "Welcome" and "SuperEditor" in bold and with a
82+
// placeholder after the word "to".
83+
final text = AttributedText(
84+
'Welcome to SuperEditor',
85+
AttributedSpans(
86+
attributions: [
87+
const SpanMarker(attribution: boldAttribution, offset: 0, markerType: SpanMarkerType.start),
88+
const SpanMarker(attribution: boldAttribution, offset: 6, markerType: SpanMarkerType.end),
89+
const SpanMarker(attribution: boldAttribution, offset: 11, markerType: SpanMarkerType.start),
90+
const SpanMarker(attribution: boldAttribution, offset: 21, markerType: SpanMarkerType.end),
91+
],
92+
),
93+
{22: const _ExamplePlaceholder()},
94+
);
95+
96+
final inlineSpan = text.computeInlineSpan(
97+
find.byType(MaterialApp).evaluate().first as BuildContext,
98+
defaultStyleBuilder,
99+
[_inlineWidgetBuilder],
100+
);
101+
102+
// Ensure the text was not modified.
103+
expect(
104+
inlineSpan.toPlainText(includePlaceholders: false),
105+
'Welcome to SuperEditor',
106+
);
107+
});
108+
});
109+
}
110+
111+
class _ExamplePlaceholder {
112+
const _ExamplePlaceholder();
113+
}
114+
115+
Widget? _inlineWidgetBuilder(BuildContext context, TextStyle textStyle, Object placeholder) {
116+
return const SizedBox(width: 10);
117+
}

0 commit comments

Comments
 (0)