diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart index fd5ccdeff3..744359052f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:example/plugin/code_block_node_widget.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -116,9 +117,17 @@ class _MyHomePageState extends State { editorState: _editorState!, editorStyle: _editorStyle, editable: true, + customBuilders: { + 'text/code_block': CodeBlockNodeWidgetBuilder(), + }, shortcutEvents: [ + enterInCodeBlock, + ignoreKeysInCodeBlock, underscoreToItalic, ], + selectionMenuItems: [ + codeBlockItem, + ], ), ); } else { diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart new file mode 100644 index 0000000000..3949073756 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart @@ -0,0 +1,277 @@ +import 'dart:collection'; + +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; +import 'package:highlight/highlight.dart' as highlight; +import 'package:highlight/languages/all.dart'; + +ShortcutEvent enterInCodeBlock = ShortcutEvent( + key: 'Enter in code block', + command: 'enter', + handler: _enterInCodeBlockHandler, +); + +ShortcutEvent ignoreKeysInCodeBlock = ShortcutEvent( + key: 'White space in code block', + command: 'space,slash,shift+underscore', + handler: _ignorekHandler, +); + +ShortcutEventHandler _enterInCodeBlockHandler = (editorState, event) { + final selection = editorState.service.selectionService.currentSelection.value; + final nodes = editorState.service.selectionService.currentSelectedNodes; + final codeBlockNode = + nodes.whereType().where((node) => node.id == 'text/code_block'); + if (codeBlockNode.length != 1 || selection == null) { + return KeyEventResult.ignored; + } + if (selection.isCollapsed) { + TransactionBuilder(editorState) + ..insertText(codeBlockNode.first, selection.end.offset, '\n') + ..commit(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; +}; + +ShortcutEventHandler _ignorekHandler = (editorState, event) { + final nodes = editorState.service.selectionService.currentSelectedNodes; + final codeBlockNodes = + nodes.whereType().where((node) => node.id == 'text/code_block'); + if (codeBlockNodes.length == 1) { + return KeyEventResult.skipRemainingHandlers; + } + return KeyEventResult.ignored; +}; + +SelectionMenuItem codeBlockItem = SelectionMenuItem( + name: 'Code Block', + icon: const Icon(Icons.abc), + keywords: ['code block'], + handler: (editorState, _, __) { + final selection = + editorState.service.selectionService.currentSelection.value; + final textNodes = editorState.service.selectionService.currentSelectedNodes + .whereType(); + if (selection == null || textNodes.isEmpty) { + return; + } + if (textNodes.first.toRawString().isEmpty) { + TransactionBuilder(editorState) + ..updateNode(textNodes.first, { + 'subtype': 'code_block', + 'theme': 'vs', + 'language': null, + }) + ..afterSelection = selection + ..commit(); + } else { + TransactionBuilder(editorState) + ..insertNode( + selection.end.path.next, + TextNode( + type: 'text', + children: LinkedList(), + attributes: { + 'subtype': 'code_block', + 'theme': 'vs', + 'language': null, + }, + delta: Delta()..insert('\n'), + ), + ) + ..afterSelection = selection + ..commit(); + } + }, +); + +class CodeBlockNodeWidgetBuilder extends NodeWidgetBuilder { + @override + Widget build(NodeWidgetContext context) { + return _CodeBlockNodeWidge( + key: context.node.key, + textNode: context.node, + editorState: context.editorState, + ); + } + + @override + NodeValidator get nodeValidator => (node) { + return node is TextNode && node.attributes['theme'] is String; + }; +} + +class _CodeBlockNodeWidge extends StatefulWidget { + const _CodeBlockNodeWidge({ + Key? key, + required this.textNode, + required this.editorState, + }) : super(key: key); + + final TextNode textNode; + final EditorState editorState; + + @override + State<_CodeBlockNodeWidge> createState() => __CodeBlockNodeWidgeState(); +} + +class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge> + with SelectableMixin, DefaultSelectable { + final _richTextKey = GlobalKey(debugLabel: 'code_block_text'); + final _padding = const EdgeInsets.only(left: 20, top: 20, bottom: 20); + String? get _language => widget.textNode.attributes['language'] as String?; + String? _detectLanguage; + + @override + SelectableMixin get forward => + _richTextKey.currentState as SelectableMixin; + + @override + GlobalKey>? get iconKey => null; + + @override + Offset get baseOffset => super.baseOffset + _padding.topLeft; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + _buildCodeBlock(context), + _buildSwitchCodeButton(context), + ], + ); + } + + Widget _buildCodeBlock(BuildContext context) { + final result = highlight.highlight.parse( + widget.textNode.toRawString(), + language: _language, + autoDetection: _language == null, + ); + _detectLanguage = _language ?? result.language; + final code = result.nodes; + final codeTextSpan = _convert(code!); + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + color: Colors.grey.withOpacity(0.1), + ), + padding: _padding, + width: MediaQuery.of(context).size.width, + child: FlowyRichText( + key: _richTextKey, + textNode: widget.textNode, + editorState: widget.editorState, + textSpanDecorator: (textSpan) => TextSpan( + style: widget.editorState.editorStyle.textStyle.defaultTextStyle, + children: codeTextSpan, + ), + ), + ); + } + + Widget _buildSwitchCodeButton(BuildContext context) { + return Positioned( + top: -5, + right: 0, + child: DropdownButton( + value: _detectLanguage, + onChanged: (value) { + TransactionBuilder(widget.editorState) + ..updateNode(widget.textNode, { + 'language': value, + }) + ..commit(); + }, + items: allLanguages.keys.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text( + value, + style: const TextStyle(fontSize: 12.0), + ), + ); + }).toList(growable: false), + ), + ); + } + + // Copy from flutter.highlight package. + // https://github.com/git-touch/highlight.dart/blob/master/flutter_highlight/lib/flutter_highlight.dart + List _convert(List nodes) { + List spans = []; + var currentSpans = spans; + List> stack = []; + + _traverse(highlight.Node node) { + if (node.value != null) { + currentSpans.add(node.className == null + ? TextSpan(text: node.value) + : TextSpan( + text: node.value, + style: _builtInCodeBlockTheme[node.className!])); + } else if (node.children != null) { + List tmp = []; + currentSpans.add(TextSpan( + children: tmp, style: _builtInCodeBlockTheme[node.className!])); + stack.add(currentSpans); + currentSpans = tmp; + + for (var n in node.children!) { + _traverse(n); + if (n == node.children!.last) { + currentSpans = stack.isEmpty ? spans : stack.removeLast(); + } + } + } + } + + for (var node in nodes) { + _traverse(node); + } + + return spans; + } +} + +const _builtInCodeBlockTheme = { + 'root': + TextStyle(backgroundColor: Color(0xffffffff), color: Color(0xff000000)), + 'comment': TextStyle(color: Color(0xff007400)), + 'quote': TextStyle(color: Color(0xff007400)), + 'tag': TextStyle(color: Color(0xffaa0d91)), + 'attribute': TextStyle(color: Color(0xffaa0d91)), + 'keyword': TextStyle(color: Color(0xffaa0d91)), + 'selector-tag': TextStyle(color: Color(0xffaa0d91)), + 'literal': TextStyle(color: Color(0xffaa0d91)), + 'name': TextStyle(color: Color(0xffaa0d91)), + 'variable': TextStyle(color: Color(0xff3F6E74)), + 'template-variable': TextStyle(color: Color(0xff3F6E74)), + 'code': TextStyle(color: Color(0xffc41a16)), + 'string': TextStyle(color: Color(0xffc41a16)), + 'meta-string': TextStyle(color: Color(0xffc41a16)), + 'regexp': TextStyle(color: Color(0xff0E0EFF)), + 'link': TextStyle(color: Color(0xff0E0EFF)), + 'title': TextStyle(color: Color(0xff1c00cf)), + 'symbol': TextStyle(color: Color(0xff1c00cf)), + 'bullet': TextStyle(color: Color(0xff1c00cf)), + 'number': TextStyle(color: Color(0xff1c00cf)), + 'section': TextStyle(color: Color(0xff643820)), + 'meta': TextStyle(color: Color(0xff643820)), + 'type': TextStyle(color: Color(0xff5c2699)), + 'built_in': TextStyle(color: Color(0xff5c2699)), + 'builtin-name': TextStyle(color: Color(0xff5c2699)), + 'params': TextStyle(color: Color(0xff5c2699)), + 'attr': TextStyle(color: Color(0xff836C28)), + 'subst': TextStyle(color: Color(0xff000000)), + 'formula': TextStyle( + backgroundColor: Color(0xffeeeeee), fontStyle: FontStyle.italic), + 'addition': TextStyle(backgroundColor: Color(0xffbaeeba)), + 'deletion': TextStyle(backgroundColor: Color(0xffffc8bd)), + 'selector-id': TextStyle(color: Color(0xff9b703f)), + 'selector-class': TextStyle(color: Color(0xff9b703f)), + 'doctag': TextStyle(fontWeight: FontWeight.bold), + 'strong': TextStyle(fontWeight: FontWeight.bold), + 'emphasis': TextStyle(fontStyle: FontStyle.italic), +}; diff --git a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml index 3c3f51632e..9f7b4e805b 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml @@ -43,6 +43,7 @@ dependencies: sdk: flutter file_picker: ^5.0.1 universal_html: ^2.0.8 + highlight: ^0.7.0 dev_dependencies: flutter_test: diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart b/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart index 0c9f447145..b594262e95 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart @@ -28,4 +28,8 @@ export 'src/service/shortcut_event/keybinding.dart'; export 'src/service/shortcut_event/shortcut_event.dart'; export 'src/service/shortcut_event/shortcut_event_handler.dart'; export 'src/extensions/attributes_extension.dart'; +export 'src/extensions/path_extensions.dart'; +export 'src/render/rich_text/default_selectable.dart'; +export 'src/render/rich_text/flowy_rich_text.dart'; +export 'src/render/selection_menu/selection_menu_widget.dart'; export 'src/l10n/l10n.dart'; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart index 81e87399b1..63e6525754 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart @@ -93,12 +93,14 @@ class Node extends ChangeNotifier with LinkedListEntry { } void updateAttributes(Attributes attributes) { - bool shouldNotifyParent = _attributes['subtype'] != attributes['subtype']; - + final oldAttributes = {..._attributes}; _attributes = composeAttributes(_attributes, attributes) ?? {}; + // Notifies the new attributes // if attributes contains 'subtype', should notify parent to rebuild node // else, just notify current node. + bool shouldNotifyParent = + _attributes['subtype'] != oldAttributes['subtype']; shouldNotifyParent ? parent?.notifyListeners() : notifyListeners(); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart index 02eaddc68d..68bb5023ca 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart @@ -8,7 +8,6 @@ import 'package:appflowy_editor/src/service/default_text_operations/format_rich_ import 'package:flutter/material.dart'; import 'package:rich_clipboard/rich_clipboard.dart'; -import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; typedef ToolbarItemEventHandler = void Function( EditorState editorState, BuildContext context); @@ -120,7 +119,7 @@ List defaultToolbarItems = [ name: 'toolbar/bold', color: isHighlight ? Colors.lightBlue : null, ), - validator: _showInTextSelection, + validator: _showInBuiltInTextSelection, highlightCallback: (editorState) => _allSatisfy( editorState, BuiltInAttributeKey.bold, @@ -136,7 +135,7 @@ List defaultToolbarItems = [ name: 'toolbar/italic', color: isHighlight ? Colors.lightBlue : null, ), - validator: _showInTextSelection, + validator: _showInBuiltInTextSelection, highlightCallback: (editorState) => _allSatisfy( editorState, BuiltInAttributeKey.italic, @@ -152,7 +151,7 @@ List defaultToolbarItems = [ name: 'toolbar/underline', color: isHighlight ? Colors.lightBlue : null, ), - validator: _showInTextSelection, + validator: _showInBuiltInTextSelection, highlightCallback: (editorState) => _allSatisfy( editorState, BuiltInAttributeKey.underline, @@ -168,7 +167,7 @@ List defaultToolbarItems = [ name: 'toolbar/strikethrough', color: isHighlight ? Colors.lightBlue : null, ), - validator: _showInTextSelection, + validator: _showInBuiltInTextSelection, highlightCallback: (editorState) => _allSatisfy( editorState, BuiltInAttributeKey.strikethrough, @@ -184,7 +183,7 @@ List defaultToolbarItems = [ name: 'toolbar/code', color: isHighlight ? Colors.lightBlue : null, ), - validator: _showInTextSelection, + validator: _showInBuiltInTextSelection, highlightCallback: (editorState) => _allSatisfy( editorState, BuiltInAttributeKey.code, @@ -248,7 +247,7 @@ List defaultToolbarItems = [ name: 'toolbar/highlight', color: isHighlight ? Colors.lightBlue : null, ), - validator: _showInTextSelection, + validator: _showInBuiltInTextSelection, highlightCallback: (editorState) => _allSatisfy( editorState, BuiltInAttributeKey.backgroundColor, @@ -262,13 +261,22 @@ List defaultToolbarItems = [ ]; ToolbarItemValidator _onlyShowInSingleTextSelection = (editorState) { + final result = _showInBuiltInTextSelection(editorState); + if (!result) { + return false; + } final nodes = editorState.service.selectionService.currentSelectedNodes; return (nodes.length == 1 && nodes.first is TextNode); }; -ToolbarItemValidator _showInTextSelection = (editorState) { +ToolbarItemValidator _showInBuiltInTextSelection = (editorState) { final nodes = editorState.service.selectionService.currentSelectedNodes - .whereType(); + .whereType() + .where( + (textNode) => + BuiltInAttributeKey.globalStyleKeys.contains(textNode.subtype) || + textNode.subtype == null, + ); return nodes.isNotEmpty; }; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart index 2655717c1d..7174290b9c 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart @@ -118,8 +118,8 @@ class _AppFlowyEditorState extends State { key: editorState.service.keyboardServiceKey, editable: widget.editable, shortcutEvents: [ - ...builtInShortcutEvents, ...widget.shortcutEvents, + ...builtInShortcutEvents, ], editorState: editorState, child: FlowyToolbar( diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart index a981470f6e..c31b8c3699 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart @@ -117,12 +117,17 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler = makeFollowingNodesIncremental(editorState, insertPath, afterSelection, beginNum: prevNumber); } else { + bool needCopyAttributes = ![ + BuiltInAttributeKey.heading, + BuiltInAttributeKey.quote, + ].contains(subtype); TransactionBuilder(editorState) ..insertNode( textNode.path, textNode.copyWith( children: LinkedList(), delta: Delta(), + attributes: needCopyAttributes ? null : {}, ), ) ..afterSelection = afterSelection @@ -173,7 +178,9 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler = Attributes _attributesFromPreviousLine(TextNode textNode) { final prevAttributes = textNode.attributes; final subType = textNode.subtype; - if (subType == null || subType == BuiltInAttributeKey.heading) { + if (subType == null || + subType == BuiltInAttributeKey.heading || + subType == BuiltInAttributeKey.quote) { return {}; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart index 0eb36fff17..0291fc34a5 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart @@ -13,12 +13,19 @@ ShortcutEventHandler tabHandler = (editorState, event) { final textNode = textNodes.first; final previous = textNode.previous; - if (textNode.subtype != BuiltInAttributeKey.bulletedList || - previous == null || - previous.subtype != BuiltInAttributeKey.bulletedList) { + + if (textNode.subtype != BuiltInAttributeKey.bulletedList) { + TransactionBuilder(editorState) + ..insertText(textNode, selection.end.offset, ' ' * 4) + ..commit(); return KeyEventResult.handled; } + if (previous == null || + previous.subtype != BuiltInAttributeKey.bulletedList) { + return KeyEventResult.ignored; + } + final path = previous.path + [previous.children.length]; final afterSelection = Selection( start: selection.start.copyWith(path: path), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart index 5259872b95..d5154bc2b5 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart @@ -124,6 +124,8 @@ class _AppFlowyKeyboardState extends State final result = shortcutEvent.handler(widget.editorState, event); if (result == KeyEventResult.handled) { return KeyEventResult.handled; + } else if (result == KeyEventResult.skipRemainingHandlers) { + return KeyEventResult.skipRemainingHandlers; } continue; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart index 6575dced25..3d3def574e 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart @@ -38,14 +38,17 @@ class _FlowyToolbarState extends State @override void showInOffset(Offset offset, LayerLink layerLink) { hide(); - + final items = _filterItems(defaultToolbarItems); + if (items.isEmpty) { + return; + } _toolbarOverlay = OverlayEntry( builder: (context) => ToolbarWidget( key: _toolbarWidgetKey, editorState: widget.editorState, layerLink: layerLink, offset: offset, - items: _filterItems(defaultToolbarItems), + items: items, ), ); Overlay.of(context)?.insert(_toolbarOverlay!); @@ -102,9 +105,4 @@ class _FlowyToolbarState extends State } return dividedItems; } - - // List _highlightItems( - // List items, - // Selection selection, - // ) {} } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart index cb2d10ea2f..916541025d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart @@ -2,7 +2,6 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../infra/test_editor.dart'; -import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; void main() async { setUpAll(() { @@ -171,13 +170,27 @@ Future _testStyleNeedToBeCopy(WidgetTester tester, String style) async { LogicalKeyboardKey.enter, ); expect(editor.documentSelection, Selection.single(path: [4], startOffset: 0)); - expect(editor.nodeAtPath([4])?.subtype, style); - await editor.pressLogicKey( - LogicalKeyboardKey.enter, - ); - expect(editor.documentSelection, Selection.single(path: [4], startOffset: 0)); - expect(editor.nodeAtPath([4])?.subtype, null); + if ([BuiltInAttributeKey.heading, BuiltInAttributeKey.quote] + .contains(style)) { + expect(editor.nodeAtPath([4])?.subtype, null); + + await editor.pressLogicKey( + LogicalKeyboardKey.enter, + ); + expect( + editor.documentSelection, Selection.single(path: [5], startOffset: 0)); + expect(editor.nodeAtPath([5])?.subtype, null); + } else { + expect(editor.nodeAtPath([4])?.subtype, style); + + await editor.pressLogicKey( + LogicalKeyboardKey.enter, + ); + expect( + editor.documentSelection, Selection.single(path: [4], startOffset: 0)); + expect(editor.nodeAtPath([4])?.subtype, null); + } } Future _testMultipleSelection( diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/tab_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/tab_handler_test.dart index 1374869deb..641282c55f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/tab_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/tab_handler_test.dart @@ -15,23 +15,24 @@ void main() async { ..insertTextNode(text) ..insertTextNode(text); await editor.startTesting(); - final document = editor.document; var selection = Selection.single(path: [0], startOffset: 0); await editor.updateSelection(selection); await editor.pressLogicKey(LogicalKeyboardKey.tab); - // nothing happens - expect(editor.documentSelection, selection); - expect(editor.document.toJson(), document.toJson()); + expect( + editor.documentSelection, + Selection.single(path: [0], startOffset: 4), + ); selection = Selection.single(path: [1], startOffset: 0); await editor.updateSelection(selection); await editor.pressLogicKey(LogicalKeyboardKey.tab); - // nothing happens - expect(editor.documentSelection, selection); - expect(editor.document.toJson(), document.toJson()); + expect( + editor.documentSelection, + Selection.single(path: [1], startOffset: 4), + ); }); testWidgets('press tab in bulleted list', (tester) async { @@ -63,7 +64,10 @@ void main() async { await editor.pressLogicKey(LogicalKeyboardKey.tab); // nothing happens - expect(editor.documentSelection, selection); + expect( + editor.documentSelection, + Selection.single(path: [0], startOffset: 0), + ); expect(editor.document.toJson(), document.toJson()); // Before