Skip to content

Commit

Permalink
[SuperEditor] Allow continuing list item sequence by typing (Resolves #…
Browse files Browse the repository at this point in the history
  • Loading branch information
angelosilvestre committed Jun 1, 2024
1 parent de1e7e5 commit c6d6c9f
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,11 @@ class UnorderedListItemConversionReaction extends ParagraphPrefixConversionReact
/// Converts a [ParagraphNode] to an [OrderedListItemNode] when the
/// user types " 1. " (or similar) at the start of the paragraph.
class OrderedListItemConversionReaction extends ParagraphPrefixConversionReaction {
static final _orderedListPattern = RegExp(r'^\s*1[.)]\s+$');
/// Matches strings like ` 1. `, ` 2. `, ` 1) `, ` 2) `, etc.
static final _orderedListPattern = RegExp(r'^\s*\d+[.)]\s+$');

/// Matches one or more numbers.
static final _numberRegex = RegExp(r'\d+');

const OrderedListItemConversionReaction();

Expand All @@ -175,6 +179,32 @@ class OrderedListItemConversionReaction extends ParagraphPrefixConversionReactio
ParagraphNode paragraph,
String match,
) {
// Extract the number from the match.
final numberMatch = _numberRegex.firstMatch(match)!;
final numberTyped = int.parse(match.substring(numberMatch.start, numberMatch.end));

if (numberTyped > 1) {
// Check if the user typed a number that continues the sequence of an upstream
// ordered list item. For example, the list has the items 1, 2, 3 and 4,
// and the user types " 5. ".

final document = editContext.find<MutableDocument>(Editor.documentKey);

final upstreamNode = document.getNodeBefore(paragraph);
if (upstreamNode == null || upstreamNode is! ListItemNode || upstreamNode.type != ListItemType.ordered) {
// There isn't an ordered list item immediately before this paragraph. Fizzle.
return;
}

// The node immediately before this paragraph is an ordered list item. Compute its ordinal value,
// so we can check if the user typed the next number in the sequence.
int upstreamListItemOrdinalValue = computeListItemOrdinalValue(upstreamNode, document);
if (numberTyped != upstreamListItemOrdinalValue + 1) {
// The user typed a number that doesn't continue the sequence of the upstream ordered list item.
return;
}
}

// The user started a paragraph with an ordered list item pattern.
// Convert the paragraph to an unordered list item.
requestDispatcher.execute([
Expand Down
45 changes: 29 additions & 16 deletions super_editor/lib/src/default_editor/list_items.dart
Original file line number Diff line number Diff line change
Expand Up @@ -113,22 +113,7 @@ class ListItemComponentBuilder implements ComponentBuilder {

int? ordinalValue;
if (node.type == ListItemType.ordered) {
// Counts the number of ordered list items above the current node with the same indentation level. Ordered
// list items with the same indentation level might be separated by ordered or unordered list items with
// different indentation levels.
ordinalValue = 1;
DocumentNode? nodeAbove = document.getNodeBefore(node);
while (nodeAbove != null && nodeAbove is ListItemNode && nodeAbove.indent >= node.indent) {
if (nodeAbove.indent == node.indent) {
if (nodeAbove.type != ListItemType.ordered) {
// We found an unordered list item with the same indentation level as the ordered list item.
// Other ordered list items aboce this one do not belong to the same list.
break;
}
ordinalValue = ordinalValue! + 1;
}
nodeAbove = document.getNodeBefore(nodeAbove);
}
ordinalValue = computeListItemOrdinalValue(node, document);
}

return switch (node.type) {
Expand Down Expand Up @@ -1198,3 +1183,31 @@ ExecutionInstruction splitListItemWhenEnterPressed({
final didSplitListItem = editContext.commonOps.insertBlockLevelNewline();
return didSplitListItem ? ExecutionInstruction.haltExecution : ExecutionInstruction.continueExecution;
}

/// Computes the ordinal value of an ordered list item.
///
/// Walks backwards counting the number of ordered list items above the [listItem] with the same indentation level.
///
/// The ordinal value starts at 1.
int computeListItemOrdinalValue(ListItemNode listItem, Document document) {
if (listItem.type != ListItemType.ordered) {
// Unordered list items do not have an ordinal value.
return 0;
}

int ordinalValue = 1;
DocumentNode? nodeAbove = document.getNodeBefore(listItem);
while (nodeAbove != null && nodeAbove is ListItemNode && nodeAbove.indent >= listItem.indent) {
if (nodeAbove.indent == listItem.indent) {
if (nodeAbove.type != ListItemType.ordered) {
// We found an unordered list item with the same indentation level as the ordered list item.
// Other ordered list items above this one do not belong to the same list.
break;
}
ordinalValue = ordinalValue + 1;
}
nodeAbove = document.getNodeBefore(nodeAbove);
}

return ordinalValue;
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,81 @@ void main() {
expect(listItemNode.text.text.isEmpty, isTrue);
}, variant: _orderedListVariant);

testWidgetsOnAllPlatforms('with a number that continues the sequence', (tester) async {
final context = await tester //
.createDocument()
.fromMarkdown('''
1. First item
2. Second item
3. Third item
''')
.withInputSource(TextInputSource.ime)
.autoFocus(true)
.pump();

final document = context.document;
await tester.placeCaretInParagraph(document.nodes[3].id, 0);

// Type a list pattern with the number 4.
await tester.typeImeText(_orderedListNumberVariant.currentValue!.replaceAll('n', '4'));

// Ensure the paragraph was converted.
final listItemNode = context.findEditContext().document.nodes[3];
expect(listItemNode, isA<ListItemNode>());
expect((listItemNode as ListItemNode).type, ListItemType.ordered);
expect(listItemNode.text.text.isEmpty, isTrue);
}, variant: _orderedListNumberVariant);

testWidgetsOnAllPlatforms('does not convert with a number that does not continues the sequence', (tester) async {
final context = await tester //
.createDocument()
.fromMarkdown('''
1. First item
2. Second item
3. Third item
''')
.withInputSource(TextInputSource.ime)
.autoFocus(true)
.pump();

final document = context.document;
await tester.placeCaretInParagraph(document.nodes[3].id, 0);

// Type a list pattern with the number 5.
final orderedListItemPattern = _orderedListNumberVariant.currentValue!.replaceAll('n', '5');
await tester.typeImeText(orderedListItemPattern);

// Ensure the paragraph was not converted and the typed text was kept.
final editingNode = context.findEditContext().document.nodes[3];
expect(editingNode, isA<ParagraphNode>());
expect((editingNode as ParagraphNode).text.text, orderedListItemPattern);
}, variant: _orderedListNumberVariant);

testWidgetsOnAllPlatforms('does not start a list with a number bigger than one', (tester) async {
final context = await tester //
.createDocument()
.withSingleEmptyParagraph()
.withInputSource(TextInputSource.ime)
.autoFocus(true)
.pump();

final document = context.document;
await tester.placeCaretInParagraph('1', 0);

// Type a list pattern with the number 2.
final orderedListItemPattern = _orderedListNumberVariant.currentValue!.replaceAll('n', '2');
await tester.typeImeText(orderedListItemPattern);

// Ensure the paragraph was not converted and the typed text was kept.
final editingNode = document.nodes.first;
expect(editingNode, isA<ParagraphNode>());
expect((editingNode as ParagraphNode).text.text, orderedListItemPattern);
}, variant: _orderedListNumberVariant);

testWidgetsOnAllPlatforms('does not convert "1 "', (tester) async {
final context = await tester //
.createDocument()
Expand Down Expand Up @@ -389,6 +464,13 @@ final _orderedListVariant = ValueVariant({
" 1) ",
});

final _orderedListNumberVariant = ValueVariant({
"n. ",
" n. ",
"n) ",
" n) ",
});

/// Holds sequence of character that shouldn't produce a horizontal rule
/// and the expected resulting text after running the editor reactions.
final _nonHrVariant = ValueVariant(const {
Expand Down

0 comments on commit c6d6c9f

Please sign in to comment.