Skip to content

Commit fd5d4e1

Browse files
angelosilvestrematthew-carroll
authored andcommitted
[SuperEditor][macOS] Fix crash after merging paragraphs containing a composing region (Resolves #2218) (#2225)
1 parent a73a81e commit fd5d4e1

File tree

2 files changed

+113
-0
lines changed

2 files changed

+113
-0
lines changed

super_editor/lib/src/default_editor/common_editor_operations.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,6 +1199,9 @@ class CommonEditorOperations {
11991199
SelectionChangeType.deleteContent,
12001200
SelectionReason.userInteraction,
12011201
),
1202+
// Since two paragraphs were combined, the composing region might point
1203+
// to a deleted paragraph. Clear it.
1204+
ClearComposingRegionRequest(),
12021205
]);
12031206

12041207
return true;

super_editor/test/super_editor/supereditor_input_ime_test.dart

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1548,6 +1548,99 @@ Paragraph two
15481548
// Ensure SuperEditor composing region was cleared.
15491549
expect(testContext.composer.composingRegion.value, isNull);
15501550
});
1551+
1552+
testWidgetsOnMac('clears composing region after merging paragraphs', (tester) async {
1553+
final testContext = await tester
1554+
.createDocument() //
1555+
.withTwoEmptyParagraphs()
1556+
.withInputSource(TextInputSource.ime)
1557+
.pump();
1558+
1559+
// Place the caret at the beginning of the second paragraph.
1560+
await tester.placeCaretInParagraph('2', 0);
1561+
1562+
// Ensure we don't have a composing region.
1563+
expect(testContext.composer.composingRegion.value, isNull);
1564+
1565+
// Simulate an insertion containing a composing region.
1566+
await tester.ime.sendDeltas(
1567+
[
1568+
TextEditingDeltaNonTextUpdate(
1569+
oldText: '. ',
1570+
selection: TextSelection.collapsed(offset: 2),
1571+
composing: TextRange(start: 2, end: 2),
1572+
),
1573+
const TextEditingDeltaInsertion(
1574+
oldText: '. ',
1575+
textInserted: 'あ',
1576+
insertionOffset: 2,
1577+
selection: TextSelection.collapsed(offset: 3),
1578+
composing: TextRange(start: 2, end: 3),
1579+
),
1580+
],
1581+
getter: imeClientGetter,
1582+
);
1583+
1584+
// Ensure the editor applied the composing region.
1585+
expect(
1586+
testContext.composer.composingRegion.value,
1587+
isNotNull,
1588+
);
1589+
1590+
// Intercept the setEditingState message sent to the platform to check if we
1591+
// cleared the IME composing region when merging paragraphs.
1592+
int? composingBase;
1593+
int? composingExtent;
1594+
tester
1595+
.interceptChannel(SystemChannels.textInput.name) //
1596+
.interceptMethod(
1597+
'TextInput.setEditingState',
1598+
(methodCall) {
1599+
final params = methodCall.arguments as Map;
1600+
composingBase = params['composingBase'];
1601+
composingExtent = params['composingExtent'];
1602+
1603+
return null;
1604+
},
1605+
);
1606+
1607+
// Simulate the user pressing BACKSPACE to delete the first character.
1608+
// Even though the selection sits after a whitespace in the IME, mac still reports
1609+
// a composing region starting after the space.
1610+
await tester.ime.sendDeltas(
1611+
[
1612+
const TextEditingDeltaDeletion(
1613+
oldText: '. あ',
1614+
deletedRange: TextRange(start: 2, end: 3),
1615+
selection: TextSelection.collapsed(offset: 2),
1616+
composing: TextRange(start: 2, end: 2),
1617+
),
1618+
],
1619+
getter: imeClientGetter,
1620+
);
1621+
1622+
// Ensure we still have a composing region in the editor.
1623+
expect(
1624+
testContext.composer.composingRegion.value,
1625+
isNotNull,
1626+
);
1627+
1628+
// Simulate the user pressing BACKSPACE to merge the paragraphs.
1629+
// Now that we are deleting a whitespace, mac reports a deleteBackward: selector
1630+
// instead of a deletion delta.
1631+
await _receiveSelector('deleteBackward:');
1632+
await tester.pump();
1633+
1634+
// Ensure the composing region was cleared in the IME.
1635+
expect(composingBase, -1);
1636+
expect(composingExtent, -1);
1637+
1638+
// Ensure SuperEditor composing region was cleared.
1639+
expect(testContext.composer.composingRegion.value, isNull);
1640+
1641+
// Ensure the paragraphs were merged.
1642+
expect(testContext.document.nodeCount, equals(1));
1643+
});
15511644
});
15521645
}
15531646

@@ -1655,3 +1748,20 @@ Future<void> _typeSpaceAdaptive(WidgetTester tester) async {
16551748

16561749
await tester.typeImeText(' ');
16571750
}
1751+
1752+
/// Simulates a `TextInputClient.performSelectors` call from the platform.
1753+
Future<void> _receiveSelector(String selectorName) async {
1754+
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
1755+
SystemChannels.textInput.name,
1756+
SystemChannels.textInput.codec.encodeMethodCall(
1757+
MethodCall(
1758+
"TextInputClient.performSelectors",
1759+
[
1760+
-1,
1761+
[selectorName],
1762+
],
1763+
),
1764+
),
1765+
null,
1766+
);
1767+
}

0 commit comments

Comments
 (0)