From e9cc4f3ad8dbe7647de22c3cafb2d0a57c7cb4c0 Mon Sep 17 00:00:00 2001 From: Mayur Mahajan Date: Mon, 10 Apr 2023 22:01:58 +0530 Subject: [PATCH 01/45] feat: make find menu widget --- .../find_replace_widget.dart | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 lib/src/render/find_replace_menu/find_replace_widget.dart diff --git a/lib/src/render/find_replace_menu/find_replace_widget.dart b/lib/src/render/find_replace_menu/find_replace_widget.dart new file mode 100644 index 000000000..e96e5017e --- /dev/null +++ b/lib/src/render/find_replace_menu/find_replace_widget.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +class FindMenuWidget extends StatefulWidget { + const FindMenuWidget({ + super.key, + required this.dismiss, + }); + + final VoidCallback dismiss; + + @override + State createState() => _FindMenuWidgetState(); +} + +class _FindMenuWidgetState extends State { + final controller = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Padding( + padding: const EdgeInsets.all(6.0), + child: SizedBox( + width: 200, + height: 50, + child: TextField( + autofocus: true, + controller: controller, + maxLines: 1, + decoration: const InputDecoration( + border: OutlineInputBorder(), + hintText: 'Enter text to search', + ), + ), + ), + ), + IconButton( + onPressed: () { + debugPrint('search button clicked'); + }, + icon: const Icon(Icons.search), + ), + IconButton( + onPressed: widget.dismiss, + icon: const Icon(Icons.cancel_outlined), + ), + ], + ); + } +} From 99a9a41d22d513f48ab4483c2f3e5562cf906eee Mon Sep 17 00:00:00 2001 From: Mayur Mahajan Date: Mon, 10 Apr 2023 22:02:16 +0530 Subject: [PATCH 02/45] feat: service for find menu --- .../find_replace_menu/find_menu_service.dart | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 lib/src/render/find_replace_menu/find_menu_service.dart diff --git a/lib/src/render/find_replace_menu/find_menu_service.dart b/lib/src/render/find_replace_menu/find_menu_service.dart new file mode 100644 index 000000000..2e3e48e7b --- /dev/null +++ b/lib/src/render/find_replace_menu/find_menu_service.dart @@ -0,0 +1,101 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/render/find_replace_menu/find_replace_widget.dart'; +import 'package:flutter/material.dart'; +import '../../editor_state.dart'; + +abstract class FindReplaceService { + void show(); + void dismiss(); +} + +class FindReplaceMenu implements FindReplaceService { + FindReplaceMenu({ + required this.context, + required this.editorState, + }); + + final BuildContext context; + final EditorState editorState; + final double topOffset = 52; + final double rightOffset = 40; + + OverlayEntry? _findReplaceMenuEntry; + bool _selectionUpdateByInner = false; + + @override + void dismiss() { + if (_findReplaceMenuEntry != null) { + editorState.service.keyboardService?.enable(); + editorState.service.scrollService?.enable(); + } + + _findReplaceMenuEntry?.remove(); + _findReplaceMenuEntry = null; + + final isSelectionDisposed = + editorState.service.selectionServiceKey.currentState == null; + if (!isSelectionDisposed) { + final selectionService = editorState.service.selectionService; + selectionService.currentSelection.removeListener(_onSelectionChange); + } + } + + @override + void show() { + dismiss(); + + final selectionService = editorState.service.selectionService; + final selectionRects = selectionService.selectionRects; + if (selectionRects.isEmpty) { + return; + } + + _findReplaceMenuEntry = OverlayEntry( + builder: (context) { + return Positioned( + top: topOffset, + right: rightOffset, + child: Material( + borderRadius: BorderRadius.circular(8.0), + child: Container( + decoration: BoxDecoration( + color: editorState.editorStyle.selectionMenuBackgroundColor ?? + Colors.white, + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 1, + color: Colors.black.withOpacity(0.1), + ), + ], + borderRadius: BorderRadius.circular(6.0), + ), + child: FindMenuWidget(dismiss: () => dismiss()), + ), + ), + ); + }, + ); + + Overlay.of(context).insert(_findReplaceMenuEntry!); + } + + void _onSelectionChange() { + // workaround: SelectionService has been released after hot reload. + final isSelectionDisposed = + editorState.service.selectionServiceKey.currentState == null; + if (!isSelectionDisposed) { + final selectionService = editorState.service.selectionService; + if (selectionService.currentSelection.value == null) { + return; + } + } + + if (_selectionUpdateByInner) { + _selectionUpdateByInner = false; + return; + } + + dismiss(); + } +} From ef9eb58d1a921c5706941f55a99265b51ac790bc Mon Sep 17 00:00:00 2001 From: Mayur Mahajan Date: Mon, 10 Apr 2023 22:02:32 +0530 Subject: [PATCH 03/45] feat: add find menu shortcut event --- .../find_replace_handler.dart | 30 +++++++++++++++++++ .../built_in_shortcut_events.dart | 8 +++++ 2 files changed, 38 insertions(+) create mode 100644 lib/src/service/internal_key_event_handlers/find_replace_handler.dart diff --git a/lib/src/service/internal_key_event_handlers/find_replace_handler.dart b/lib/src/service/internal_key_event_handlers/find_replace_handler.dart new file mode 100644 index 000000000..8a2da8c6d --- /dev/null +++ b/lib/src/service/internal_key_event_handlers/find_replace_handler.dart @@ -0,0 +1,30 @@ +import 'package:appflowy_editor/src/core/document/node.dart'; +import 'package:appflowy_editor/src/extensions/node_extensions.dart'; +import 'package:appflowy_editor/src/render/find_replace_menu/find_menu_service.dart'; +import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; +import 'package:flutter/material.dart'; + +FindReplaceService? _findMenuService; +ShortcutEventHandler findShortcutHandler = (editorState, event) { + final textNodes = editorState.service.selectionService.currentSelectedNodes + .whereType(); + if (textNodes.length != 1) { + return KeyEventResult.ignored; + } + + final selection = editorState.service.selectionService.currentSelection.value; + final textNode = textNodes.first; + final context = textNode.context; + final selectable = textNode.selectable; + if (selection == null || context == null || selectable == null) { + return KeyEventResult.ignored; + } + + WidgetsBinding.instance.addPostFrameCallback((_) { + _findMenuService = + FindReplaceMenu(context: context, editorState: editorState); + _findMenuService?.show(); + }); + + return KeyEventResult.handled; +}; diff --git a/lib/src/service/shortcut_event/built_in_shortcut_events.dart b/lib/src/service/shortcut_event/built_in_shortcut_events.dart index 63d86319e..06cdf7ef4 100644 --- a/lib/src/service/shortcut_event/built_in_shortcut_events.dart +++ b/lib/src/service/shortcut_event/built_in_shortcut_events.dart @@ -4,6 +4,7 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/copy_pas import 'package:appflowy_editor/src/service/internal_key_event_handlers/cursor_left_delete_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/exit_editing_mode_handler.dart'; +import 'package:appflowy_editor/src/service/internal_key_event_handlers/find_replace_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/markdown_syntax_to_styled_text.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/outdent_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/page_up_down_handler.dart'; @@ -259,6 +260,13 @@ List builtInShortcutEvents = [ character: '/', handler: slashShortcutHandler, ), + ShortcutEvent( + key: 'Find', + command: 'meta+f', + windowsCommand: 'ctrl+f', + linuxCommand: 'ctrl+f', + handler: findShortcutHandler, + ), ShortcutEvent( key: 'enter', command: 'enter', From c95eb997a03a6072b13f46a3f9f3641cad96a037 Mon Sep 17 00:00:00 2001 From: Mayur Mahajan Date: Tue, 11 Apr 2023 19:30:10 +0530 Subject: [PATCH 04/45] feat: create a search service --- .../find_replace_menu/find_menu_service.dart | 5 +- .../find_replace_widget.dart | 27 +++-- .../find_replace_menu/search_service.dart | 101 ++++++++++++++++++ 3 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 lib/src/render/find_replace_menu/search_service.dart diff --git a/lib/src/render/find_replace_menu/find_menu_service.dart b/lib/src/render/find_replace_menu/find_menu_service.dart index 2e3e48e7b..d3b1cd3bb 100644 --- a/lib/src/render/find_replace_menu/find_menu_service.dart +++ b/lib/src/render/find_replace_menu/find_menu_service.dart @@ -70,7 +70,10 @@ class FindReplaceMenu implements FindReplaceService { ], borderRadius: BorderRadius.circular(6.0), ), - child: FindMenuWidget(dismiss: () => dismiss()), + child: FindMenuWidget( + dismiss: dismiss, + editorState: editorState, + ), ), ), ); diff --git a/lib/src/render/find_replace_menu/find_replace_widget.dart b/lib/src/render/find_replace_menu/find_replace_widget.dart index e96e5017e..262ec5ccd 100644 --- a/lib/src/render/find_replace_menu/find_replace_widget.dart +++ b/lib/src/render/find_replace_menu/find_replace_widget.dart @@ -1,12 +1,16 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/render/find_replace_menu/search_service.dart'; import 'package:flutter/material.dart'; class FindMenuWidget extends StatefulWidget { const FindMenuWidget({ super.key, required this.dismiss, + required this.editorState, }); final VoidCallback dismiss; + final EditorState editorState; @override State createState() => _FindMenuWidgetState(); @@ -14,6 +18,15 @@ class FindMenuWidget extends StatefulWidget { class _FindMenuWidgetState extends State { final controller = TextEditingController(); + late SearchService searchService; + + @override + void initState() { + super.initState(); + searchService = SearchService( + editorState: widget.editorState, + ); + } @override Widget build(BuildContext context) { @@ -27,7 +40,8 @@ class _FindMenuWidgetState extends State { child: TextField( autofocus: true, controller: controller, - maxLines: 1, + onSubmitted: (_) => + searchService.findAndHighlight(controller.text), decoration: const InputDecoration( border: OutlineInputBorder(), hintText: 'Enter text to search', @@ -36,14 +50,15 @@ class _FindMenuWidgetState extends State { ), ), IconButton( - onPressed: () { - debugPrint('search button clicked'); - }, + onPressed: () => searchService.findAndHighlight(controller.text), icon: const Icon(Icons.search), ), IconButton( - onPressed: widget.dismiss, - icon: const Icon(Icons.cancel_outlined), + onPressed: () { + widget.dismiss(); + searchService.unHighlight(controller.text); + }, + icon: const Icon(Icons.close), ), ], ); diff --git a/lib/src/render/find_replace_menu/search_service.dart b/lib/src/render/find_replace_menu/search_service.dart new file mode 100644 index 000000000..93144c5d9 --- /dev/null +++ b/lib/src/render/find_replace_menu/search_service.dart @@ -0,0 +1,101 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; +import 'dart:math' as math; + +class SearchService { + SearchService({ + required this.editorState, + }); + + final EditorState editorState; + Map> nodeMatchMap = {}; + + void findAndHighlight(String pattern) { + final contents = editorState.document.root.children; + + if (contents.isEmpty || pattern.isEmpty) return; + + final firstNode = contents.firstWhere( + (element) => element is TextNode, + ); + + final lastNode = contents.lastWhere( + (element) => element is TextNode, + ); + + final nodes = NodeIterator( + document: editorState.document, + startNode: firstNode, + endNode: lastNode, + ).toList(); + + for (var n in nodes) { + if (n is TextNode) { + //we will try to find the pattern using bayer moore search. + List matches = boyerMooreSearch(pattern, n.toPlainText()); + nodeMatchMap[n] = matches; + highlightMatches(n.path, matches, pattern.length); + } + } + } + + void highlightMatches(Path path, List matches, int patternLength) { + for (var match in matches) { + Position start = Position(path: path, offset: match); + Position end = Position(path: path, offset: match + patternLength); + + editorState.updateCursorSelection(Selection(start: start, end: end)); + + formatHighlight( + editorState, + editorState.editorStyle.highlightColorHex!, + ); + } + } + + void unHighlight(String pattern) { + findAndHighlight(pattern); + } + + List boyerMooreSearch(String pattern, String text) { + int m = pattern.length; + int n = text.length; + + Map badchar = {}; + List matches = []; + + badCharHeuristic(pattern, m, badchar); + + int s = 0; + + while (s <= (n - m)) { + int j = m - 1; + + while (j >= 0 && pattern[j] == text[s + j]) { + j--; + } + + //if pattern is present at current shift, the index will become -1 + if (j < 0) { + matches.add(s); + s += (s + m < n) ? m - (badchar[text[s + m]] ?? -1) : 1; + } else { + s += math.max(1, j - (badchar[text[s + j]] ?? -1)); + } + } + + return matches; + } + + void badCharHeuristic(String pat, int size, Map badchar) { + // Initialize all occurrences as -1 + badchar.clear(); + + // Fill the actual value of last occurrence + // of a character (indices of table are characters and values are index of occurrence) + for (int i = 0; i < size; i++) { + String ch = pat[i]; + badchar[ch] = i; + } + } +} From 150d49ec762f0d0ac4bc9b2622c5531b36ec9d76 Mon Sep 17 00:00:00 2001 From: Mayur Mahajan Date: Sun, 23 Apr 2023 13:47:20 +0530 Subject: [PATCH 05/45] docs: explain search service --- .../find_replace_menu/find_menu_service.dart | 2 +- .../find_replace_menu/search_service.dart | 36 +++++++++++++------ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/lib/src/render/find_replace_menu/find_menu_service.dart b/lib/src/render/find_replace_menu/find_menu_service.dart index d3b1cd3bb..61509702d 100644 --- a/lib/src/render/find_replace_menu/find_menu_service.dart +++ b/lib/src/render/find_replace_menu/find_menu_service.dart @@ -57,7 +57,7 @@ class FindReplaceMenu implements FindReplaceService { right: rightOffset, child: Material( borderRadius: BorderRadius.circular(8.0), - child: Container( + child: DecoratedBox( decoration: BoxDecoration( color: editorState.editorStyle.selectionMenuBackgroundColor ?? Colors.white, diff --git a/lib/src/render/find_replace_menu/search_service.dart b/lib/src/render/find_replace_menu/search_service.dart index 93144c5d9..6b4b07eb9 100644 --- a/lib/src/render/find_replace_menu/search_service.dart +++ b/lib/src/render/find_replace_menu/search_service.dart @@ -1,5 +1,4 @@ import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; import 'dart:math' as math; class SearchService { @@ -10,6 +9,9 @@ class SearchService { final EditorState editorState; Map> nodeMatchMap = {}; + /// Finds the pattern in editorState.document and stores it in a + /// map. Calls the highlightMatch method to highlight the pattern + /// if it is found. void findAndHighlight(String pattern) { final contents = editorState.document.root.children; @@ -23,27 +25,39 @@ class SearchService { (element) => element is TextNode, ); + //iterate within all the text nodes of the document. final nodes = NodeIterator( document: editorState.document, startNode: firstNode, endNode: lastNode, ).toList(); - for (var n in nodes) { + //traversing all the nodes + for (final n in nodes) { if (n is TextNode) { - //we will try to find the pattern using bayer moore search. - List matches = boyerMooreSearch(pattern, n.toPlainText()); + //matches list will contain the offsets where the desired word, + //is found. + List matches = _boyerMooreSearch(pattern, n.toPlainText()); + //we will store this list of offsets in a hashmap where each node, + //will be the respective key and its matches will be its value. nodeMatchMap[n] = matches; + //finally we will highlight all the mathces. highlightMatches(n.path, matches, pattern.length); } } } + /// This method takes in the TextNode's path, matches is a list of offsets, + /// patternLength is the length of the word which is being searched. + /// + /// So for example: path= 1, offset= 10, and patternLength= 5 will mean + /// that the word is located on path 1 from [1,10] to [1,14] void highlightMatches(Path path, List matches, int patternLength) { - for (var match in matches) { + for (final match in matches) { Position start = Position(path: path, offset: match); Position end = Position(path: path, offset: match + patternLength); + //we select the matched word and hide the toolbar. editorState.updateCursorSelection(Selection(start: start, end: end)); formatHighlight( @@ -57,14 +71,15 @@ class SearchService { findAndHighlight(pattern); } - List boyerMooreSearch(String pattern, String text) { + //this is a standard algorithm used for searching patterns in long text samples + List _boyerMooreSearch(String pattern, String text) { int m = pattern.length; int n = text.length; Map badchar = {}; List matches = []; - badCharHeuristic(pattern, m, badchar); + _badCharHeuristic(pattern, m, badchar); int s = 0; @@ -87,12 +102,11 @@ class SearchService { return matches; } - void badCharHeuristic(String pat, int size, Map badchar) { - // Initialize all occurrences as -1 + void _badCharHeuristic(String pat, int size, Map badchar) { badchar.clear(); - // Fill the actual value of last occurrence - // of a character (indices of table are characters and values are index of occurrence) + // Fill the actual value of last occurrence of a character + // (indices of table are characters and values are index of occurrence) for (int i = 0; i < size; i++) { String ch = pat[i]; badchar[ch] = i; From e29bb40343b83d3dd44f3a7c73e9dd8f939ee24a Mon Sep 17 00:00:00 2001 From: Mayur Mahajan Date: Sun, 23 Apr 2023 14:09:53 +0530 Subject: [PATCH 06/45] fix: unhighlight method takes searched word --- .../find_replace_widget.dart | 18 ++++++++--- .../find_replace_menu/search_service.dart | 31 +++++++++++-------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/lib/src/render/find_replace_menu/find_replace_widget.dart b/lib/src/render/find_replace_menu/find_replace_widget.dart index 262ec5ccd..e596081a2 100644 --- a/lib/src/render/find_replace_menu/find_replace_widget.dart +++ b/lib/src/render/find_replace_menu/find_replace_widget.dart @@ -18,6 +18,7 @@ class FindMenuWidget extends StatefulWidget { class _FindMenuWidgetState extends State { final controller = TextEditingController(); + String queriedPattern = ''; late SearchService searchService; @override @@ -40,8 +41,7 @@ class _FindMenuWidgetState extends State { child: TextField( autofocus: true, controller: controller, - onSubmitted: (_) => - searchService.findAndHighlight(controller.text), + onSubmitted: (_) => _searchPattern(), decoration: const InputDecoration( border: OutlineInputBorder(), hintText: 'Enter text to search', @@ -50,17 +50,27 @@ class _FindMenuWidgetState extends State { ), ), IconButton( - onPressed: () => searchService.findAndHighlight(controller.text), + onPressed: () => _searchPattern(), icon: const Icon(Icons.search), ), IconButton( onPressed: () { widget.dismiss(); - searchService.unHighlight(controller.text); + searchService.unHighlight(queriedPattern); + setState(() { + queriedPattern = ''; + }); }, icon: const Icon(Icons.close), ), ], ); } + + void _searchPattern() { + searchService.findAndHighlight(controller.text); + setState(() { + queriedPattern = controller.text; + }); + } } diff --git a/lib/src/render/find_replace_menu/search_service.dart b/lib/src/render/find_replace_menu/search_service.dart index 6b4b07eb9..a208eda27 100644 --- a/lib/src/render/find_replace_menu/search_service.dart +++ b/lib/src/render/find_replace_menu/search_service.dart @@ -7,10 +7,13 @@ class SearchService { }); final EditorState editorState; - Map> nodeMatchMap = {}; + //matchedPositions will contain a list of positions of the matched patterns + //the position here consists of the node and the starting offset of the + //matched pattern. We will use this to traverse between the matched patterns. + List matchedPositions = []; - /// Finds the pattern in editorState.document and stores it in a - /// map. Calls the highlightMatch method to highlight the pattern + /// Finds the pattern in editorState.document and stores it in matchedPositions. + /// Calls the highlightMatch method to highlight the pattern /// if it is found. void findAndHighlight(String pattern) { final contents = editorState.document.root.children; @@ -38,21 +41,27 @@ class SearchService { //matches list will contain the offsets where the desired word, //is found. List matches = _boyerMooreSearch(pattern, n.toPlainText()); - //we will store this list of offsets in a hashmap where each node, - //will be the respective key and its matches will be its value. - nodeMatchMap[n] = matches; + //we will store this list of offsets along with their path, + //in a list of positions. + for (int matchedOffset in matches) { + matchedPositions.add(Position(path: n.path, offset: matchedOffset)); + } //finally we will highlight all the mathces. - highlightMatches(n.path, matches, pattern.length); + _highlightMatches(n.path, matches, pattern.length); } } } + void unHighlight(String pattern) { + findAndHighlight(pattern); + } + /// This method takes in the TextNode's path, matches is a list of offsets, - /// patternLength is the length of the word which is being searched. + /// patternLength is the length of the word which is being searched. /// /// So for example: path= 1, offset= 10, and patternLength= 5 will mean /// that the word is located on path 1 from [1,10] to [1,14] - void highlightMatches(Path path, List matches, int patternLength) { + void _highlightMatches(Path path, List matches, int patternLength) { for (final match in matches) { Position start = Position(path: path, offset: match); Position end = Position(path: path, offset: match + patternLength); @@ -67,10 +76,6 @@ class SearchService { } } - void unHighlight(String pattern) { - findAndHighlight(pattern); - } - //this is a standard algorithm used for searching patterns in long text samples List _boyerMooreSearch(String pattern, String text) { int m = pattern.length; From 3d32f3417804c8de1b24c4666909016e8297a894 Mon Sep 17 00:00:00 2001 From: Mayur Mahajan Date: Fri, 28 Apr 2023 22:45:04 +0530 Subject: [PATCH 07/45] fix: unhighlight before each search --- lib/src/render/find_replace_menu/search_service.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/src/render/find_replace_menu/search_service.dart b/lib/src/render/find_replace_menu/search_service.dart index a208eda27..5208f5746 100644 --- a/lib/src/render/find_replace_menu/search_service.dart +++ b/lib/src/render/find_replace_menu/search_service.dart @@ -11,11 +11,19 @@ class SearchService { //the position here consists of the node and the starting offset of the //matched pattern. We will use this to traverse between the matched patterns. List matchedPositions = []; + String queriedPattern = ''; /// Finds the pattern in editorState.document and stores it in matchedPositions. /// Calls the highlightMatch method to highlight the pattern /// if it is found. void findAndHighlight(String pattern) { + if (queriedPattern != pattern) { + //this means we have a new pattern, but before we highlight the new matches, + //lets unhiglight the old pattern + unHighlight(queriedPattern); + queriedPattern = pattern; + } + final contents = editorState.document.root.children; if (contents.isEmpty || pattern.isEmpty) return; From c79bcecd14c6d084f2634194f9bff294da9b8264 Mon Sep 17 00:00:00 2001 From: Mayur Mahajan Date: Fri, 28 Apr 2023 23:16:20 +0530 Subject: [PATCH 08/45] feat: navigate between matches --- .../find_replace_widget.dart | 12 ++++++ .../find_replace_menu/search_service.dart | 38 +++++++++++++++++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/lib/src/render/find_replace_menu/find_replace_widget.dart b/lib/src/render/find_replace_menu/find_replace_widget.dart index e596081a2..8d4e927de 100644 --- a/lib/src/render/find_replace_menu/find_replace_widget.dart +++ b/lib/src/render/find_replace_menu/find_replace_widget.dart @@ -53,6 +53,14 @@ class _FindMenuWidgetState extends State { onPressed: () => _searchPattern(), icon: const Icon(Icons.search), ), + IconButton( + onPressed: () => _navigateToMatchedIndex(moveUp: true), + icon: const Icon(Icons.arrow_upward), + ), + IconButton( + onPressed: () => _navigateToMatchedIndex(), + icon: const Icon(Icons.arrow_downward), + ), IconButton( onPressed: () { widget.dismiss(); @@ -73,4 +81,8 @@ class _FindMenuWidgetState extends State { queriedPattern = controller.text; }); } + + void _navigateToMatchedIndex({bool moveUp = false}) { + searchService.navigateToMatch(moveUp); + } } diff --git a/lib/src/render/find_replace_menu/search_service.dart b/lib/src/render/find_replace_menu/search_service.dart index 5208f5746..3f2e06034 100644 --- a/lib/src/render/find_replace_menu/search_service.dart +++ b/lib/src/render/find_replace_menu/search_service.dart @@ -12,6 +12,7 @@ class SearchService { //matched pattern. We will use this to traverse between the matched patterns. List matchedPositions = []; String queriedPattern = ''; + int selectedIndex = 0; /// Finds the pattern in editorState.document and stores it in matchedPositions. /// Calls the highlightMatch method to highlight the pattern @@ -58,12 +59,35 @@ class SearchService { _highlightMatches(n.path, matches, pattern.length); } } + + selectedIndex = matchedPositions.length - 1; } void unHighlight(String pattern) { findAndHighlight(pattern); } + /// This method takes in a boolean parameter moveUp, if set to true, + /// the match located above the current selected match is newly selected. + /// Otherwise the match below the current selected match is newly selected. + void navigateToMatch(bool moveUp) { + if (moveUp) { + selectedIndex = + selectedIndex - 1 < 0 ? matchedPositions.length - 1 : --selectedIndex; + + final match = matchedPositions[selectedIndex]; + _selectWordAtPosition(match); + //FIXME: selecting a word should scroll editor automatically. + } else { + selectedIndex = + (selectedIndex + 1) < matchedPositions.length ? ++selectedIndex : 0; + + final match = matchedPositions[selectedIndex]; + _selectWordAtPosition(match); + //FIXME: selecting a word should scroll editor automatically. + } + } + /// This method takes in the TextNode's path, matches is a list of offsets, /// patternLength is the length of the word which is being searched. /// @@ -72,10 +96,7 @@ class SearchService { void _highlightMatches(Path path, List matches, int patternLength) { for (final match in matches) { Position start = Position(path: path, offset: match); - Position end = Position(path: path, offset: match + patternLength); - - //we select the matched word and hide the toolbar. - editorState.updateCursorSelection(Selection(start: start, end: end)); + _selectWordAtPosition(start); formatHighlight( editorState, @@ -84,6 +105,15 @@ class SearchService { } } + void _selectWordAtPosition(Position start) { + Position end = Position( + path: start.path, + offset: start.offset + queriedPattern.length, + ); + + editorState.updateCursorSelection(Selection(start: start, end: end)); + } + //this is a standard algorithm used for searching patterns in long text samples List _boyerMooreSearch(String pattern, String text) { int m = pattern.length; From 2e167a026d260ed5761e14cc7af0b52535cd4344 Mon Sep 17 00:00:00 2001 From: Mayur Mahajan Date: Fri, 28 Apr 2023 23:31:37 +0530 Subject: [PATCH 09/45] feat: forget highlighting from undo stack --- lib/src/history/undo_manager.dart | 12 ++++++++++++ lib/src/render/find_replace_menu/search_service.dart | 1 + 2 files changed, 13 insertions(+) diff --git a/lib/src/history/undo_manager.dart b/lib/src/history/undo_manager.dart index c3e8b8f95..da881e1b6 100644 --- a/lib/src/history/undo_manager.dart +++ b/lib/src/history/undo_manager.dart @@ -149,4 +149,16 @@ class UndoManager { ), ); } + + void forgetRecentUndo() { + Log.editor.debug('forgetRecentUndo'); + final s = state; + if (s == null) { + return; + } + final historyItem = undoStack.pop(); + if (historyItem == null) { + return; + } + } } diff --git a/lib/src/render/find_replace_menu/search_service.dart b/lib/src/render/find_replace_menu/search_service.dart index 3f2e06034..5bee700a4 100644 --- a/lib/src/render/find_replace_menu/search_service.dart +++ b/lib/src/render/find_replace_menu/search_service.dart @@ -102,6 +102,7 @@ class SearchService { editorState, editorState.editorStyle.highlightColorHex!, ); + editorState.undoManager.forgetRecentUndo(); } } From ac4b94b8b1ab278b03c4095ccf80d0dd182e8cd3 Mon Sep 17 00:00:00 2001 From: Mayur Mahajan Date: Sun, 30 Apr 2023 12:29:58 +0530 Subject: [PATCH 10/45] feat: replace logic and ui --- .../find_replace_widget.dart | 137 +++++++++++++----- .../find_replace_menu/search_service.dart | 60 +++++++- 2 files changed, 157 insertions(+), 40 deletions(-) diff --git a/lib/src/render/find_replace_menu/find_replace_widget.dart b/lib/src/render/find_replace_menu/find_replace_widget.dart index 8d4e927de..3ec671ed2 100644 --- a/lib/src/render/find_replace_menu/find_replace_widget.dart +++ b/lib/src/render/find_replace_menu/find_replace_widget.dart @@ -17,8 +17,10 @@ class FindMenuWidget extends StatefulWidget { } class _FindMenuWidgetState extends State { - final controller = TextEditingController(); + final findController = TextEditingController(); + final replaceController = TextEditingController(); String queriedPattern = ''; + bool replaceFlag = false; late SearchService searchService; @override @@ -31,58 +33,115 @@ class _FindMenuWidgetState extends State { @override Widget build(BuildContext context) { - return Row( + return Column( children: [ - Padding( - padding: const EdgeInsets.all(6.0), - child: SizedBox( - width: 200, - height: 50, - child: TextField( - autofocus: true, - controller: controller, - onSubmitted: (_) => _searchPattern(), - decoration: const InputDecoration( - border: OutlineInputBorder(), - hintText: 'Enter text to search', + Row( + children: [ + IconButton( + onPressed: () => setState(() { + replaceFlag = !replaceFlag; + }), + icon: replaceFlag + ? const Icon(Icons.expand_less) + : const Icon(Icons.expand_more), + ), + Padding( + padding: const EdgeInsets.all(6.0), + child: SizedBox( + width: 200, + height: 50, + child: TextField( + autofocus: true, + controller: findController, + onSubmitted: (_) => _searchPattern(), + decoration: const InputDecoration( + border: OutlineInputBorder(), + hintText: 'Enter text to search', + ), + ), ), ), - ), - ), - IconButton( - onPressed: () => _searchPattern(), - icon: const Icon(Icons.search), - ), - IconButton( - onPressed: () => _navigateToMatchedIndex(moveUp: true), - icon: const Icon(Icons.arrow_upward), - ), - IconButton( - onPressed: () => _navigateToMatchedIndex(), - icon: const Icon(Icons.arrow_downward), - ), - IconButton( - onPressed: () { - widget.dismiss(); - searchService.unHighlight(queriedPattern); - setState(() { - queriedPattern = ''; - }); - }, - icon: const Icon(Icons.close), + IconButton( + onPressed: () => _navigateToMatchedIndex(moveUp: true), + icon: const Icon(Icons.arrow_upward), + tooltip: 'Previous Match', + ), + IconButton( + onPressed: () => _navigateToMatchedIndex(), + icon: const Icon(Icons.arrow_downward), + tooltip: 'Next Match', + ), + IconButton( + onPressed: () { + widget.dismiss(); + searchService.unHighlight(queriedPattern); + setState(() { + queriedPattern = ''; + }); + }, + icon: const Icon(Icons.close), + tooltip: 'Close', + ), + ], ), + replaceFlag + ? Row( + children: [ + Padding( + padding: const EdgeInsets.all(6.0), + child: SizedBox( + width: 200, + height: 50, + child: TextField( + autofocus: false, + controller: replaceController, + onSubmitted: (_) => _replaceSelectedWord(), + decoration: const InputDecoration( + border: OutlineInputBorder(), + hintText: 'Replace', + ), + ), + ), + ), + IconButton( + onPressed: () => _replaceSelectedWord(), + icon: const Icon(Icons.find_replace), + tooltip: 'Replace', + ), + IconButton( + onPressed: () => _replaceAllMatches(), + icon: const Icon(Icons.change_circle_outlined), + tooltip: 'Replace All', + ), + ], + ) + : const SizedBox(height: 0), ], ); } void _searchPattern() { - searchService.findAndHighlight(controller.text); + searchService.findAndHighlight(findController.text); setState(() { - queriedPattern = controller.text; + queriedPattern = findController.text; }); } void _navigateToMatchedIndex({bool moveUp = false}) { searchService.navigateToMatch(moveUp); } + + void _replaceSelectedWord() { + if (findController.text != queriedPattern) { + _searchPattern(); + } + searchService.replaceSelectedWord(replaceController.text); + } + + void _replaceAllMatches() { + if (findController.text != queriedPattern) { + _searchPattern(); + } + searchService.replaceAllMatches(replaceController.text); + } } diff --git a/lib/src/render/find_replace_menu/search_service.dart b/lib/src/render/find_replace_menu/search_service.dart index 5bee700a4..848081a7d 100644 --- a/lib/src/render/find_replace_menu/search_service.dart +++ b/lib/src/render/find_replace_menu/search_service.dart @@ -71,6 +71,7 @@ class SearchService { /// the match located above the current selected match is newly selected. /// Otherwise the match below the current selected match is newly selected. void navigateToMatch(bool moveUp) { + if (matchedPositions.isEmpty) return; if (moveUp) { selectedIndex = selectedIndex - 1 < 0 ? matchedPositions.length - 1 : --selectedIndex; @@ -88,6 +89,61 @@ class SearchService { } } + /// Replaces the current selected word with replaceText. + /// After replacing the selected word, this method selects the next + /// matched word if that exists. + void replaceSelectedWord(String replaceText) { + if (replaceText.isEmpty || + queriedPattern.isEmpty || + matchedPositions.isEmpty) { + return; + } + + final matchedPosition = matchedPositions[selectedIndex]; + _selectWordAtPosition(matchedPosition); + + //unhighlight the selected word before it is replaced + formatHighlight( + editorState, + editorState.editorStyle.highlightColorHex!, + ); + editorState.undoManager.forgetRecentUndo(); + + final textNode = editorState.service.selectionService.currentSelectedNodes + .whereType() + .first; + + final transaction = editorState.transaction; + + transaction.replaceText( + textNode, + matchedPosition.offset, + queriedPattern.length, + replaceText, + ); + + editorState.apply(transaction); + + matchedPositions.removeAt(selectedIndex); + navigateToMatch(false); + } + + /// Replaces all the found occurances of pattern with replaceText + void replaceAllMatches(String replaceText) { + if (replaceText.isEmpty || queriedPattern.isEmpty) { + return; + } + //we need to create a final variable matchesLength here, because + //when we replaceSelectedWord we reduce the length of matchedPositions + //list, this causes the value to shrink dynamically and thus it may + //result in pretermination. + final int matchesLength = matchedPositions.length; + + for (int i = 0; i < matchesLength; i++) { + replaceSelectedWord(replaceText); + } + } + /// This method takes in the TextNode's path, matches is a list of offsets, /// patternLength is the length of the word which is being searched. /// @@ -115,7 +171,9 @@ class SearchService { editorState.updateCursorSelection(Selection(start: start, end: end)); } - //this is a standard algorithm used for searching patterns in long text samples + //This is a standard algorithm used for searching patterns in long text samples + //It is more efficient than brute force searching because it is able to skip + //characters that will never possibly match with required pattern. List _boyerMooreSearch(String pattern, String text) { int m = pattern.length; int n = text.length; From fd5c08e796cedb1c620cef6e976b30c95e784026 Mon Sep 17 00:00:00 2001 From: Mayur Mahajan Date: Mon, 1 May 2023 09:38:36 +0530 Subject: [PATCH 11/45] feat: replace shortcut handler and widget --- .../find_replace_menu/find_menu_service.dart | 3 ++ .../find_replace_widget.dart | 13 +++---- .../find_replace_menu/search_service.dart | 4 +-- .../find_replace_handler.dart | 34 +++++++++++++++++-- .../built_in_shortcut_events.dart | 7 ++++ 5 files changed, 51 insertions(+), 10 deletions(-) diff --git a/lib/src/render/find_replace_menu/find_menu_service.dart b/lib/src/render/find_replace_menu/find_menu_service.dart index 61509702d..0fa1bfe97 100644 --- a/lib/src/render/find_replace_menu/find_menu_service.dart +++ b/lib/src/render/find_replace_menu/find_menu_service.dart @@ -12,10 +12,12 @@ class FindReplaceMenu implements FindReplaceService { FindReplaceMenu({ required this.context, required this.editorState, + required this.replaceFlag, }); final BuildContext context; final EditorState editorState; + final bool replaceFlag; final double topOffset = 52; final double rightOffset = 40; @@ -73,6 +75,7 @@ class FindReplaceMenu implements FindReplaceService { child: FindMenuWidget( dismiss: dismiss, editorState: editorState, + replaceFlag: replaceFlag, ), ), ), diff --git a/lib/src/render/find_replace_menu/find_replace_widget.dart b/lib/src/render/find_replace_menu/find_replace_widget.dart index 3ec671ed2..a02f951ff 100644 --- a/lib/src/render/find_replace_menu/find_replace_widget.dart +++ b/lib/src/render/find_replace_menu/find_replace_widget.dart @@ -7,10 +7,12 @@ class FindMenuWidget extends StatefulWidget { super.key, required this.dismiss, required this.editorState, + required this.replaceFlag, }); final VoidCallback dismiss; final EditorState editorState; + final bool replaceFlag; @override State createState() => _FindMenuWidgetState(); @@ -26,6 +28,9 @@ class _FindMenuWidgetState extends State { @override void initState() { super.initState(); + setState(() { + replaceFlag = widget.replaceFlag; + }); searchService = SearchService( editorState: widget.editorState, ); @@ -62,12 +67,12 @@ class _FindMenuWidgetState extends State { ), ), IconButton( - onPressed: () => _navigateToMatchedIndex(moveUp: true), + onPressed: () => searchService.navigateToMatch(moveUp: true), icon: const Icon(Icons.arrow_upward), tooltip: 'Previous Match', ), IconButton( - onPressed: () => _navigateToMatchedIndex(), + onPressed: () => searchService.navigateToMatch(), icon: const Icon(Icons.arrow_downward), tooltip: 'Next Match', ), @@ -127,10 +132,6 @@ class _FindMenuWidgetState extends State { }); } - void _navigateToMatchedIndex({bool moveUp = false}) { - searchService.navigateToMatch(moveUp); - } - void _replaceSelectedWord() { if (findController.text != queriedPattern) { _searchPattern(); diff --git a/lib/src/render/find_replace_menu/search_service.dart b/lib/src/render/find_replace_menu/search_service.dart index 848081a7d..634485b23 100644 --- a/lib/src/render/find_replace_menu/search_service.dart +++ b/lib/src/render/find_replace_menu/search_service.dart @@ -70,7 +70,7 @@ class SearchService { /// This method takes in a boolean parameter moveUp, if set to true, /// the match located above the current selected match is newly selected. /// Otherwise the match below the current selected match is newly selected. - void navigateToMatch(bool moveUp) { + void navigateToMatch({bool moveUp = false}) { if (matchedPositions.isEmpty) return; if (moveUp) { selectedIndex = @@ -125,7 +125,7 @@ class SearchService { editorState.apply(transaction); matchedPositions.removeAt(selectedIndex); - navigateToMatch(false); + navigateToMatch(moveUp: false); } /// Replaces all the found occurances of pattern with replaceText diff --git a/lib/src/service/internal_key_event_handlers/find_replace_handler.dart b/lib/src/service/internal_key_event_handlers/find_replace_handler.dart index 8a2da8c6d..345d51a37 100644 --- a/lib/src/service/internal_key_event_handlers/find_replace_handler.dart +++ b/lib/src/service/internal_key_event_handlers/find_replace_handler.dart @@ -21,8 +21,38 @@ ShortcutEventHandler findShortcutHandler = (editorState, event) { } WidgetsBinding.instance.addPostFrameCallback((_) { - _findMenuService = - FindReplaceMenu(context: context, editorState: editorState); + _findMenuService = FindReplaceMenu( + context: context, + editorState: editorState, + replaceFlag: false, + ); + _findMenuService?.show(); + }); + + return KeyEventResult.handled; +}; + +ShortcutEventHandler replaceShortcutHandler = (editorState, event) { + final textNodes = editorState.service.selectionService.currentSelectedNodes + .whereType(); + if (textNodes.length != 1) { + return KeyEventResult.ignored; + } + + final selection = editorState.service.selectionService.currentSelection.value; + final textNode = textNodes.first; + final context = textNode.context; + final selectable = textNode.selectable; + if (selection == null || context == null || selectable == null) { + return KeyEventResult.ignored; + } + + WidgetsBinding.instance.addPostFrameCallback((_) { + _findMenuService = FindReplaceMenu( + context: context, + editorState: editorState, + replaceFlag: true, + ); _findMenuService?.show(); }); diff --git a/lib/src/service/shortcut_event/built_in_shortcut_events.dart b/lib/src/service/shortcut_event/built_in_shortcut_events.dart index 06cdf7ef4..ae0d7aca0 100644 --- a/lib/src/service/shortcut_event/built_in_shortcut_events.dart +++ b/lib/src/service/shortcut_event/built_in_shortcut_events.dart @@ -267,6 +267,13 @@ List builtInShortcutEvents = [ linuxCommand: 'ctrl+f', handler: findShortcutHandler, ), + ShortcutEvent( + key: 'Replace', + command: 'meta+h', + windowsCommand: 'ctrl+h', + linuxCommand: 'ctrl+h', + handler: replaceShortcutHandler, + ), ShortcutEvent( key: 'enter', command: 'enter', From 65221b8df88e72e08776014cb89b013437ef56bf Mon Sep 17 00:00:00 2001 From: Mayur Mahajan Date: Mon, 1 May 2023 17:05:02 +0530 Subject: [PATCH 12/45] test: find functionality --- .../find_replace_widget.dart | 5 + .../find_replace_menu_test.dart | 424 ++++++++++++++++++ 2 files changed, 429 insertions(+) create mode 100644 test/render/find_replace_menu/find_replace_menu_test.dart diff --git a/lib/src/render/find_replace_menu/find_replace_widget.dart b/lib/src/render/find_replace_menu/find_replace_widget.dart index a02f951ff..11cf1579e 100644 --- a/lib/src/render/find_replace_menu/find_replace_widget.dart +++ b/lib/src/render/find_replace_menu/find_replace_widget.dart @@ -56,6 +56,7 @@ class _FindMenuWidgetState extends State { width: 200, height: 50, child: TextField( + key: const Key('findTextField'), autofocus: true, controller: findController, onSubmitted: (_) => _searchPattern(), @@ -67,16 +68,19 @@ class _FindMenuWidgetState extends State { ), ), IconButton( + key: const Key('previousMatchButton'), onPressed: () => searchService.navigateToMatch(moveUp: true), icon: const Icon(Icons.arrow_upward), tooltip: 'Previous Match', ), IconButton( + key: const Key('nextMatchButton'), onPressed: () => searchService.navigateToMatch(), icon: const Icon(Icons.arrow_downward), tooltip: 'Next Match', ), IconButton( + key: const Key('closeButton'), onPressed: () { widget.dismiss(); searchService.unHighlight(queriedPattern); @@ -98,6 +102,7 @@ class _FindMenuWidgetState extends State { width: 200, height: 50, child: TextField( + key: const Key('replaceTextField'), autofocus: false, controller: replaceController, onSubmitted: (_) => _replaceSelectedWord(), diff --git a/test/render/find_replace_menu/find_replace_menu_test.dart b/test/render/find_replace_menu/find_replace_menu_test.dart new file mode 100644 index 000000000..623620621 --- /dev/null +++ b/test/render/find_replace_menu/find_replace_menu_test.dart @@ -0,0 +1,424 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:appflowy_editor/src/render/find_replace_menu/find_replace_widget.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import '../../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('find_replace_menu.dart', () { + testWidgets('find menu appears properly', (tester) async { + await _prepare(tester, lines: 3); + + //the prepare method only checks if FindMenuWidget is present + //so here we also check if FindMenuWidget contains TextField + //and IconButtons or not. + expect(find.byType(TextField), findsOneWidget); + expect(find.byType(IconButton), findsAtLeastNWidgets(4)); + }); + + testWidgets('find menu disappears when close is called', (tester) async { + await _prepare(tester, lines: 3); + + //lets check if find menu disappears if the close button is tapped. + await tester.tap(find.byKey(const Key('closeButton'))); + await tester.pumpAndSettle(); + + expect(find.byType(FindMenuWidget), findsNothing); + expect(find.byType(TextField), findsNothing); + expect(find.byType(IconButton), findsNothing); + }); + + testWidgets('find menu does not work with empty input', (tester) async { + const textInputKey = Key('findTextField'); + final editor = await _prepare(tester); + + await tester.tap(find.byKey(textInputKey)); + await tester.enterText(find.byKey(textInputKey), ''); + await tester.pumpAndSettle(); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + //pressing enter should trigger the findAndHighlight method, but + //since the input is empty the document is not affected. + await editor.pressLogicKey( + key: LogicalKeyboardKey.enter, + ); + + //since the method will not select anything as searched pattern is + //empty, the current selection should be equal to previous selection. + final selection = + editor.editorState.service.selectionService.currentSelection.value; + + expect(selection, Selection.single(path: [0], startOffset: 0)); + + //we can do this because there is only one text node. + final textNode = editor.nodeAtPath([0]) as TextNode; + + //we expect that nothing is highlighted in our current document. + expect( + textNode.allSatisfyInSelection( + selection!, + BuiltInAttributeKey.backgroundColor, + (value) => value == '0x00000000', + ), + true, + ); + }); + + testWidgets('find menu works properly when match is not found', + (tester) async { + const pattern = 'Flutter'; + const textInputKey = Key('findTextField'); + + final editor = await _prepare(tester, lines: 3); + + await tester.tap(find.byKey(textInputKey)); + await tester.enterText(find.byKey(textInputKey), pattern); + await tester.pumpAndSettle(); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + //pressing enter should trigger the findAndHighlight method, which + //will find the pattern inside the editor. + await editor.pressLogicKey( + key: LogicalKeyboardKey.enter, + ); + + //fetching the current selection + final selection = + editor.editorState.service.selectionService.currentSelection.value; + + //since no match is found the current selection should not be different + //from initial selection. + expect(selection != null, true); + expect(selection, Selection.single(path: [0], startOffset: 0)); + }); + + testWidgets('found matches are highlighted', (tester) async { + const pattern = 'Welcome'; + + final editor = await _prepareWithTextInputForFind( + tester, + lines: 3, + pattern: pattern, + ); + + //checking if current selection consists an occurance of matched pattern. + final selection = + editor.editorState.service.selectionService.currentSelection.value; + + //we expect the last occurance of the pattern to be found, thus that should + //be the current selection. + expect(selection != null, true); + expect(selection!.start, Position(path: [2], offset: 0)); + expect(selection.end, Position(path: [2], offset: pattern.length)); + + //check whether the node with found occurance of patten is highlighted + final textNode = editor.nodeAtPath([2]) as TextNode; + + //we expect that the current selected node is highlighted. + //we can confirm that by saying that the node's backgroung color is not white. + expect( + textNode.allSatisfyInSelection( + selection, + BuiltInAttributeKey.backgroundColor, + (value) => value != '0x00000000', + ), + true, + ); + }); + + testWidgets('navigating to previous matches works', (tester) async { + const pattern = 'Welcome'; + const previousBtnKey = Key('previousMatchButton'); + + final editor = await _prepareWithTextInputForFind( + tester, + lines: 2, + pattern: pattern, + ); + + //checking if current selection consists an occurance of matched pattern. + var selection = + editor.editorState.service.selectionService.currentSelection.value; + + //we expect the last occurance of the pattern to be found, thus that should + //be the current selection. + expect(selection != null, true); + expect(selection!.start, Position(path: [1], offset: 0)); + expect(selection.end, Position(path: [1], offset: pattern.length)); + + //now pressing the icon button for previous match should select + //node at path [0]. + await tester.tap(find.byKey(previousBtnKey)); + await tester.pumpAndSettle(); + + selection = + editor.editorState.service.selectionService.currentSelection.value; + + expect(selection != null, true); + expect(selection!.start, Position(path: [0], offset: 0)); + expect(selection.end, Position(path: [0], offset: pattern.length)); + + //now pressing the icon button for previous match should select + //node at path [1], since there is no node before node at [0]. + await tester.tap(find.byKey(previousBtnKey)); + await tester.pumpAndSettle(); + + selection = + editor.editorState.service.selectionService.currentSelection.value; + + expect(selection != null, true); + expect(selection!.start, Position(path: [1], offset: 0)); + expect(selection.end, Position(path: [1], offset: pattern.length)); + }); + + testWidgets('navigating to next matches works', (tester) async { + const pattern = 'Welcome'; + const nextBtnKey = Key('nextMatchButton'); + + final editor = await _prepareWithTextInputForFind( + tester, + lines: 3, + pattern: pattern, + ); + + //the last found occurance should be selected + var selection = + editor.editorState.service.selectionService.currentSelection.value; + + expect(selection != null, true); + expect(selection!.start, Position(path: [2], offset: 0)); + expect(selection.end, Position(path: [2], offset: pattern.length)); + + //now pressing the icon button for next match should select + //node at path [0], since there are no nodes after node at [2]. + await tester.tap(find.byKey(nextBtnKey)); + await tester.pumpAndSettle(); + + selection = + editor.editorState.service.selectionService.currentSelection.value; + + expect(selection != null, true); + expect(selection!.start, Position(path: [0], offset: 0)); + expect(selection.end, Position(path: [0], offset: pattern.length)); + + //now pressing the icon button for previous match should select + //node at path [1]. + await tester.tap(find.byKey(nextBtnKey)); + await tester.pumpAndSettle(); + + selection = + editor.editorState.service.selectionService.currentSelection.value; + + expect(selection != null, true); + expect(selection!.start, Position(path: [1], offset: 0)); + expect(selection.end, Position(path: [1], offset: pattern.length)); + }); + + testWidgets('found matches are unhighlighted when findMenu closed', + (tester) async { + const pattern = 'Welcome'; + const closeBtnKey = Key('closeButton'); + + final editor = await _prepareWithTextInputForFind( + tester, + lines: 3, + pattern: pattern, + ); + + var selection = + editor.editorState.service.selectionService.currentSelection.value; + + final textNode = editor.nodeAtPath([2]) as TextNode; + + //node is highlighted while menu is active + expect( + textNode.allSatisfyInSelection( + selection!, + BuiltInAttributeKey.backgroundColor, + (value) => value != '0x00000000', + ), + true, + ); + + //presses the close button + await tester.tap(find.byKey(closeBtnKey)); + await tester.pumpAndSettle(); + + //closes the findMenuWidget + expect(find.byType(FindMenuWidget), findsNothing); + + //node is unhighlighted after the menu is closed + expect( + textNode.allSatisfyInSelection( + selection, + BuiltInAttributeKey.backgroundColor, + (value) => value == '0x00000000', + ), + true, + ); + }); + + testWidgets('old matches are unhighlighted when new pattern is searched', + (tester) async { + const textInputKey = Key('findTextField'); + + const textLine1 = 'Welcome to Appflowy 😁'; + const textLine2 = 'Appflowy is made with Flutter, Rust and ❤️'; + var pattern = 'Welcome'; + + final editor = tester.editor + ..insertTextNode(textLine1) + ..insertTextNode(textLine2); + + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + + if (Platform.isWindows || Platform.isLinux) { + await editor.pressLogicKey( + key: LogicalKeyboardKey.keyF, + isControlPressed: true, + ); + } else { + await editor.pressLogicKey( + key: LogicalKeyboardKey.keyF, + isMetaPressed: true, + ); + } + + await tester.pumpAndSettle(const Duration(milliseconds: 1000)); + + await tester.tap(find.byKey(textInputKey)); + await tester.enterText(find.byKey(textInputKey), pattern); + await tester.pumpAndSettle(); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + //finds the pattern + await editor.pressLogicKey( + key: LogicalKeyboardKey.enter, + ); + + //since node at path [1] does not contain match, we expect it + //to be not highlighted. + var selection = Selection.single(path: [1], startOffset: 0); + var textNode = editor.nodeAtPath([1]) as TextNode; + + expect( + textNode.allSatisfyInSelection( + selection, + BuiltInAttributeKey.backgroundColor, + (value) => value == '0x00000000', + ), + true, + ); + + //now we will change the pattern to Flutter and search it + pattern = 'Flutter'; + await tester.tap(find.byKey(textInputKey)); + await tester.enterText(find.byKey(textInputKey), pattern); + await tester.pumpAndSettle(); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + //finds the pattern Flutter + await editor.pressLogicKey( + key: LogicalKeyboardKey.enter, + ); + + //now we expect the text node at path 1 to contain highlighted pattern + expect( + textNode.allSatisfyInSelection( + selection, + BuiltInAttributeKey.backgroundColor, + (value) => value != '0x00000000', + ), + true, + ); + }); + }); +} + +Future _prepare( + WidgetTester tester, { + int lines = 1, +}) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor; + for (var i = 0; i < lines; i++) { + editor.insertTextNode(text); + } + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + + if (Platform.isWindows || Platform.isLinux) { + await editor.pressLogicKey( + key: LogicalKeyboardKey.keyF, + isControlPressed: true, + ); + } else { + await editor.pressLogicKey( + key: LogicalKeyboardKey.keyF, + isMetaPressed: true, + ); + } + + await tester.pumpAndSettle(const Duration(milliseconds: 1000)); + + expect(find.byType(FindMenuWidget), findsOneWidget); + + return Future.value(editor); +} + +Future _prepareWithTextInputForFind( + WidgetTester tester, { + int lines = 1, + String pattern = "Welcome", +}) async { + const text = 'Welcome to Appflowy 😁'; + const textInputKey = Key('findTextField'); + final editor = tester.editor; + for (var i = 0; i < lines; i++) { + editor.insertTextNode(text); + } + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + + if (Platform.isWindows || Platform.isLinux) { + await editor.pressLogicKey( + key: LogicalKeyboardKey.keyF, + isControlPressed: true, + ); + } else { + await editor.pressLogicKey( + key: LogicalKeyboardKey.keyF, + isMetaPressed: true, + ); + } + + await tester.pumpAndSettle(const Duration(milliseconds: 1000)); + + expect(find.byType(FindMenuWidget), findsOneWidget); + + await tester.tap(find.byKey(textInputKey)); + await tester.enterText(find.byKey(textInputKey), pattern); + await tester.pumpAndSettle(); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + //pressing enter should trigger the findAndHighlight method, which + //will find the pattern inside the editor. + await editor.pressLogicKey( + key: LogicalKeyboardKey.enter, + ); + + return Future.value(editor); +} From 0b0ad4dae3b8ae4862b2fb2f04e60716dc5eaa9d Mon Sep 17 00:00:00 2001 From: Mayur Mahajan Date: Mon, 1 May 2023 23:27:55 +0530 Subject: [PATCH 13/45] test: replace menu tests --- .../find_replace_widget.dart | 1 + ....dart => find_replace_menu_find_test.dart} | 37 +-- .../find_replace_menu_replace_test.dart | 242 ++++++++++++++++++ 3 files changed, 254 insertions(+), 26 deletions(-) rename test/render/find_replace_menu/{find_replace_menu_test.dart => find_replace_menu_find_test.dart} (91%) create mode 100644 test/render/find_replace_menu/find_replace_menu_replace_test.dart diff --git a/lib/src/render/find_replace_menu/find_replace_widget.dart b/lib/src/render/find_replace_menu/find_replace_widget.dart index 11cf1579e..f0136ea62 100644 --- a/lib/src/render/find_replace_menu/find_replace_widget.dart +++ b/lib/src/render/find_replace_menu/find_replace_widget.dart @@ -119,6 +119,7 @@ class _FindMenuWidgetState extends State { tooltip: 'Replace', ), IconButton( + key: const Key('replaceAllButton'), onPressed: () => _replaceAllMatches(), icon: const Icon(Icons.change_circle_outlined), tooltip: 'Replace All', diff --git a/test/render/find_replace_menu/find_replace_menu_test.dart b/test/render/find_replace_menu/find_replace_menu_find_test.dart similarity index 91% rename from test/render/find_replace_menu/find_replace_menu_test.dart rename to test/render/find_replace_menu/find_replace_menu_find_test.dart index 623620621..38034ee79 100644 --- a/test/render/find_replace_menu/find_replace_menu_test.dart +++ b/test/render/find_replace_menu/find_replace_menu_find_test.dart @@ -12,7 +12,7 @@ void main() async { TestWidgetsFlutterBinding.ensureInitialized(); }); - group('find_replace_menu.dart', () { + group('find_replace_menu.dart findMenu', () { testWidgets('find menu appears properly', (tester) async { await _prepare(tester, lines: 3); @@ -36,19 +36,13 @@ void main() async { }); testWidgets('find menu does not work with empty input', (tester) async { - const textInputKey = Key('findTextField'); - final editor = await _prepare(tester); - - await tester.tap(find.byKey(textInputKey)); - await tester.enterText(find.byKey(textInputKey), ''); - await tester.pumpAndSettle(); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); + const pattern = ''; - //pressing enter should trigger the findAndHighlight method, but - //since the input is empty the document is not affected. - await editor.pressLogicKey( - key: LogicalKeyboardKey.enter, + //we are passing empty string for pattern + final editor = await _prepareWithTextInputForFind( + tester, + lines: 1, + pattern: pattern, ); //since the method will not select anything as searched pattern is @@ -75,20 +69,11 @@ void main() async { testWidgets('find menu works properly when match is not found', (tester) async { const pattern = 'Flutter'; - const textInputKey = Key('findTextField'); - final editor = await _prepare(tester, lines: 3); - - await tester.tap(find.byKey(textInputKey)); - await tester.enterText(find.byKey(textInputKey), pattern); - await tester.pumpAndSettle(); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - - //pressing enter should trigger the findAndHighlight method, which - //will find the pattern inside the editor. - await editor.pressLogicKey( - key: LogicalKeyboardKey.enter, + final editor = await _prepareWithTextInputForFind( + tester, + lines: 1, + pattern: pattern, ); //fetching the current selection diff --git a/test/render/find_replace_menu/find_replace_menu_replace_test.dart b/test/render/find_replace_menu/find_replace_menu_replace_test.dart new file mode 100644 index 000000000..98d14a94e --- /dev/null +++ b/test/render/find_replace_menu/find_replace_menu_replace_test.dart @@ -0,0 +1,242 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:appflowy_editor/src/render/find_replace_menu/find_replace_widget.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import '../../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('find_replace_menu.dart replaceMenu', () { + testWidgets('replace menu appears properly', (tester) async { + await _prepare(tester, lines: 3); + + //the prepare method only checks if FindMenuWidget is present + //so here we also check if FindMenuWidget contains TextField + //and IconButtons or not. + //and whether there are two textfields for replace menu as well. + expect(find.byType(TextField), findsNWidgets(2)); + expect(find.byType(IconButton), findsAtLeastNWidgets(6)); + }); + + testWidgets('replace menu disappears when close is called', (tester) async { + await _prepare(tester, lines: 3); + + await tester.tap(find.byKey(const Key('closeButton'))); + await tester.pumpAndSettle(); + + expect(find.byType(FindMenuWidget), findsNothing); + expect(find.byType(TextField), findsNothing); + expect(find.byType(IconButton), findsNothing); + }); + + testWidgets('replace menu does not work when find is not called', + (tester) async { + const textInputKey = Key('replaceTextField'); + const pattern = 'Flutter'; + final editor = await _prepare(tester); + + await tester.tap(find.byKey(textInputKey)); + await tester.enterText(find.byKey(textInputKey), pattern); + await tester.pumpAndSettle(); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + //pressing enter should trigger the replaceSelected method + await editor.pressLogicKey( + key: LogicalKeyboardKey.enter, + ); + await tester.pumpAndSettle(); + + //note our document only has one node + final textNode = editor.nodeAtPath([0]) as TextNode; + const expectedText = 'Welcome to Appflowy 😁'; + expect(textNode.toPlainText(), expectedText); + }); + + testWidgets('replace does not change text when no match is found', + (tester) async { + const textInputKey = Key('replaceTextField'); + const pattern = 'Flutter'; + + final editor = await _prepareWithTextInputForFind( + tester, + lines: 1, + pattern: pattern, + ); + + await tester.tap(find.byKey(textInputKey)); + await tester.enterText(find.byKey(textInputKey), pattern); + await tester.pumpAndSettle(); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + await editor.pressLogicKey( + key: LogicalKeyboardKey.enter, + ); + await tester.pumpAndSettle(); + + final textNode = editor.nodeAtPath([0]) as TextNode; + const expectedText = 'Welcome to Appflowy 😁'; + expect(textNode.toPlainText(), expectedText); + }); + + testWidgets('found selected match is replaced properly', (tester) async { + const patternToBeFound = 'Welcome'; + const replacePattern = 'Salute'; + const textInputKey = Key('replaceTextField'); + + final editor = await _prepareWithTextInputForFind( + tester, + lines: 3, + pattern: patternToBeFound, + ); + + //check if matches are not yet replaced + var textNode = editor.nodeAtPath([2]) as TextNode; + var expectedText = '$patternToBeFound to Appflowy 😁'; + expect(textNode.toPlainText(), expectedText); + + //we select the replace text field and provide replacePattern + await tester.tap(find.byKey(textInputKey)); + await tester.enterText(find.byKey(textInputKey), replacePattern); + await tester.pumpAndSettle(); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + await editor.pressLogicKey( + key: LogicalKeyboardKey.enter, + ); + + await tester.pumpAndSettle(); + + //we know that the findAndHighlight method selects the last + //matched occurance in the editor document. + textNode = editor.nodeAtPath([2]) as TextNode; + expectedText = '$replacePattern to Appflowy 😁'; + expect(textNode.toPlainText(), expectedText); + + //also check if other matches are not yet replaced + textNode = editor.nodeAtPath([1]) as TextNode; + expectedText = '$patternToBeFound to Appflowy 😁'; + expect(textNode.toPlainText(), expectedText); + }); + + testWidgets('replace all on found matches', (tester) async { + const patternToBeFound = 'Welcome'; + const replacePattern = 'Salute'; + const expectedText = '$replacePattern to Appflowy 😁'; + const lines = 3; + + const textInputKey = Key('replaceTextField'); + const replaceAllBtn = Key('replaceAllButton'); + + final editor = await _prepareWithTextInputForFind( + tester, + lines: lines, + pattern: patternToBeFound, + ); + + //check if matches are not yet replaced + var textNode = editor.nodeAtPath([2]) as TextNode; + var originalText = '$patternToBeFound to Appflowy 😁'; + expect(textNode.toPlainText(), originalText); + + await tester.tap(find.byKey(textInputKey)); + await tester.enterText(find.byKey(textInputKey), replacePattern); + await tester.pumpAndSettle(); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(replaceAllBtn)); + await tester.pumpAndSettle(); + + //all matches should be replaced + for (var i = 0; i < lines; i++) { + textNode = editor.nodeAtPath([i]) as TextNode; + expect(textNode.toPlainText(), expectedText); + } + }); + }); +} + +Future _prepare( + WidgetTester tester, { + int lines = 1, +}) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor; + for (var i = 0; i < lines; i++) { + editor.insertTextNode(text); + } + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + + if (Platform.isWindows || Platform.isLinux) { + await editor.pressLogicKey( + key: LogicalKeyboardKey.keyH, + isControlPressed: true, + ); + } else { + await editor.pressLogicKey( + key: LogicalKeyboardKey.keyH, + isMetaPressed: true, + ); + } + + await tester.pumpAndSettle(const Duration(milliseconds: 1000)); + + expect(find.byType(FindMenuWidget), findsOneWidget); + + return Future.value(editor); +} + +Future _prepareWithTextInputForFind( + WidgetTester tester, { + int lines = 1, + String pattern = "Welcome", +}) async { + const text = 'Welcome to Appflowy 😁'; + const textInputKey = Key('findTextField'); + final editor = tester.editor; + for (var i = 0; i < lines; i++) { + editor.insertTextNode(text); + } + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + + if (Platform.isWindows || Platform.isLinux) { + await editor.pressLogicKey( + key: LogicalKeyboardKey.keyH, + isControlPressed: true, + ); + } else { + await editor.pressLogicKey( + key: LogicalKeyboardKey.keyH, + isMetaPressed: true, + ); + } + + await tester.pumpAndSettle(const Duration(milliseconds: 1000)); + + expect(find.byType(FindMenuWidget), findsOneWidget); + + await tester.tap(find.byKey(textInputKey)); + await tester.enterText(find.byKey(textInputKey), pattern); + await tester.pumpAndSettle(); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + //pressing enter should trigger the findAndHighlight method, which + //will find the pattern inside the editor. + await editor.pressLogicKey( + key: LogicalKeyboardKey.enter, + ); + + return Future.value(editor); +} From 6909b32a644b1c6ea8b2b364340f896f46bb078a Mon Sep 17 00:00:00 2001 From: Mayur Mahajan Date: Thu, 11 May 2023 18:27:49 +0530 Subject: [PATCH 14/45] refactor: separate class for search algo --- .../find_replace_menu/search_algorithm.dart | 47 +++++++++++++++++ .../find_replace_menu/search_service.dart | 50 ++----------------- 2 files changed, 51 insertions(+), 46 deletions(-) create mode 100644 lib/src/render/find_replace_menu/search_algorithm.dart diff --git a/lib/src/render/find_replace_menu/search_algorithm.dart b/lib/src/render/find_replace_menu/search_algorithm.dart new file mode 100644 index 000000000..8e468aac8 --- /dev/null +++ b/lib/src/render/find_replace_menu/search_algorithm.dart @@ -0,0 +1,47 @@ +import 'dart:math' as math; + +class BayerMooreAlgorithm { + //This is a standard algorithm used for searching patterns in long text samples + //It is more efficient than brute force searching because it is able to skip + //characters that will never possibly match with required pattern. + List boyerMooreSearch(String pattern, String text) { + int m = pattern.length; + int n = text.length; + + Map badchar = {}; + List matches = []; + + _badCharHeuristic(pattern, m, badchar); + + int s = 0; + + while (s <= (n - m)) { + int j = m - 1; + + while (j >= 0 && pattern[j] == text[s + j]) { + j--; + } + + //if pattern is present at current shift, the index will become -1 + if (j < 0) { + matches.add(s); + s += (s + m < n) ? m - (badchar[text[s + m]] ?? -1) : 1; + } else { + s += math.max(1, j - (badchar[text[s + j]] ?? -1)); + } + } + + return matches; + } + + void _badCharHeuristic(String pat, int size, Map badchar) { + badchar.clear(); + + // Fill the actual value of last occurrence of a character + // (indices of table are characters and values are index of occurrence) + for (int i = 0; i < size; i++) { + String ch = pat[i]; + badchar[ch] = i; + } + } +} diff --git a/lib/src/render/find_replace_menu/search_service.dart b/lib/src/render/find_replace_menu/search_service.dart index 634485b23..8b9287ee9 100644 --- a/lib/src/render/find_replace_menu/search_service.dart +++ b/lib/src/render/find_replace_menu/search_service.dart @@ -1,5 +1,5 @@ import 'package:appflowy_editor/appflowy_editor.dart'; -import 'dart:math' as math; +import 'package:appflowy_editor/src/render/find_replace_menu/search_algorithm.dart'; class SearchService { SearchService({ @@ -11,6 +11,7 @@ class SearchService { //the position here consists of the node and the starting offset of the //matched pattern. We will use this to traverse between the matched patterns. List matchedPositions = []; + BayerMooreAlgorithm searchAlgorithm = BayerMooreAlgorithm(); String queriedPattern = ''; int selectedIndex = 0; @@ -49,7 +50,8 @@ class SearchService { if (n is TextNode) { //matches list will contain the offsets where the desired word, //is found. - List matches = _boyerMooreSearch(pattern, n.toPlainText()); + List matches = + searchAlgorithm.boyerMooreSearch(pattern, n.toPlainText()); //we will store this list of offsets along with their path, //in a list of positions. for (int matchedOffset in matches) { @@ -170,48 +172,4 @@ class SearchService { editorState.updateCursorSelection(Selection(start: start, end: end)); } - - //This is a standard algorithm used for searching patterns in long text samples - //It is more efficient than brute force searching because it is able to skip - //characters that will never possibly match with required pattern. - List _boyerMooreSearch(String pattern, String text) { - int m = pattern.length; - int n = text.length; - - Map badchar = {}; - List matches = []; - - _badCharHeuristic(pattern, m, badchar); - - int s = 0; - - while (s <= (n - m)) { - int j = m - 1; - - while (j >= 0 && pattern[j] == text[s + j]) { - j--; - } - - //if pattern is present at current shift, the index will become -1 - if (j < 0) { - matches.add(s); - s += (s + m < n) ? m - (badchar[text[s + m]] ?? -1) : 1; - } else { - s += math.max(1, j - (badchar[text[s + j]] ?? -1)); - } - } - - return matches; - } - - void _badCharHeuristic(String pat, int size, Map badchar) { - badchar.clear(); - - // Fill the actual value of last occurrence of a character - // (indices of table are characters and values are index of occurrence) - for (int i = 0; i < size; i++) { - String ch = pat[i]; - badchar[ch] = i; - } - } } From 329d094bae92156df811a3b305563345752996c3 Mon Sep 17 00:00:00 2001 From: Mayur Mahajan Date: Thu, 11 May 2023 19:25:46 +0530 Subject: [PATCH 15/45] refactor: suggested changes --- lib/src/history/undo_manager.dart | 8 ++------ lib/src/render/find_replace_menu/find_replace_widget.dart | 8 +++----- lib/src/render/find_replace_menu/search_service.dart | 4 ++-- .../find_replace_menu/find_replace_menu_find_test.dart | 2 +- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/lib/src/history/undo_manager.dart b/lib/src/history/undo_manager.dart index da881e1b6..53f56dc7e 100644 --- a/lib/src/history/undo_manager.dart +++ b/lib/src/history/undo_manager.dart @@ -152,13 +152,9 @@ class UndoManager { void forgetRecentUndo() { Log.editor.debug('forgetRecentUndo'); - final s = state; - if (s == null) { - return; - } - final historyItem = undoStack.pop(); - if (historyItem == null) { + if (state == null) { return; } + undoStack.pop(); } } diff --git a/lib/src/render/find_replace_menu/find_replace_widget.dart b/lib/src/render/find_replace_menu/find_replace_widget.dart index f0136ea62..7cd7ed7bc 100644 --- a/lib/src/render/find_replace_menu/find_replace_widget.dart +++ b/lib/src/render/find_replace_menu/find_replace_widget.dart @@ -28,9 +28,7 @@ class _FindMenuWidgetState extends State { @override void initState() { super.initState(); - setState(() { - replaceFlag = widget.replaceFlag; - }); + replaceFlag = widget.replaceFlag; searchService = SearchService( editorState: widget.editorState, ); @@ -83,7 +81,7 @@ class _FindMenuWidgetState extends State { key: const Key('closeButton'), onPressed: () { widget.dismiss(); - searchService.unHighlight(queriedPattern); + searchService.unhighlight(queriedPattern); setState(() { queriedPattern = ''; }); @@ -126,7 +124,7 @@ class _FindMenuWidgetState extends State { ), ], ) - : const SizedBox(height: 0), + : const SizedBox.shrink(), ], ); } diff --git a/lib/src/render/find_replace_menu/search_service.dart b/lib/src/render/find_replace_menu/search_service.dart index 8b9287ee9..581adca51 100644 --- a/lib/src/render/find_replace_menu/search_service.dart +++ b/lib/src/render/find_replace_menu/search_service.dart @@ -22,7 +22,7 @@ class SearchService { if (queriedPattern != pattern) { //this means we have a new pattern, but before we highlight the new matches, //lets unhiglight the old pattern - unHighlight(queriedPattern); + unhighlight(queriedPattern); queriedPattern = pattern; } @@ -65,7 +65,7 @@ class SearchService { selectedIndex = matchedPositions.length - 1; } - void unHighlight(String pattern) { + void unhighlight(String pattern) { findAndHighlight(pattern); } diff --git a/test/render/find_replace_menu/find_replace_menu_find_test.dart b/test/render/find_replace_menu/find_replace_menu_find_test.dart index 38034ee79..92276b17e 100644 --- a/test/render/find_replace_menu/find_replace_menu_find_test.dart +++ b/test/render/find_replace_menu/find_replace_menu_find_test.dart @@ -219,7 +219,7 @@ void main() async { pattern: pattern, ); - var selection = + final selection = editor.editorState.service.selectionService.currentSelection.value; final textNode = editor.nodeAtPath([2]) as TextNode; From 8e278a2c7c52b6a928b53d636cba7086fb8d3113 Mon Sep 17 00:00:00 2001 From: Mayur Mahajan Date: Sat, 13 May 2023 11:58:51 +0530 Subject: [PATCH 16/45] refactor: remove unhighlight method --- lib/src/render/find_replace_menu/find_replace_widget.dart | 2 +- lib/src/render/find_replace_menu/search_service.dart | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/src/render/find_replace_menu/find_replace_widget.dart b/lib/src/render/find_replace_menu/find_replace_widget.dart index 7cd7ed7bc..d5b44ab22 100644 --- a/lib/src/render/find_replace_menu/find_replace_widget.dart +++ b/lib/src/render/find_replace_menu/find_replace_widget.dart @@ -81,7 +81,7 @@ class _FindMenuWidgetState extends State { key: const Key('closeButton'), onPressed: () { widget.dismiss(); - searchService.unhighlight(queriedPattern); + searchService.findAndHighlight(queriedPattern); setState(() { queriedPattern = ''; }); diff --git a/lib/src/render/find_replace_menu/search_service.dart b/lib/src/render/find_replace_menu/search_service.dart index 581adca51..ae3164bc5 100644 --- a/lib/src/render/find_replace_menu/search_service.dart +++ b/lib/src/render/find_replace_menu/search_service.dart @@ -22,7 +22,7 @@ class SearchService { if (queriedPattern != pattern) { //this means we have a new pattern, but before we highlight the new matches, //lets unhiglight the old pattern - unhighlight(queriedPattern); + findAndHighlight(queriedPattern); queriedPattern = pattern; } @@ -65,10 +65,6 @@ class SearchService { selectedIndex = matchedPositions.length - 1; } - void unhighlight(String pattern) { - findAndHighlight(pattern); - } - /// This method takes in a boolean parameter moveUp, if set to true, /// the match located above the current selected match is newly selected. /// Otherwise the match below the current selected match is newly selected. From 77986fc69b48264e026933ee76cdde483cac594c Mon Sep 17 00:00:00 2001 From: Mayur Mahajan Date: Sun, 21 May 2023 11:28:42 +0530 Subject: [PATCH 17/45] feat: add find highlight color --- lib/src/render/find_replace_menu/search_service.dart | 4 ++-- lib/src/render/style/editor_style.dart | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/src/render/find_replace_menu/search_service.dart b/lib/src/render/find_replace_menu/search_service.dart index ae3164bc5..61bb2bd8b 100644 --- a/lib/src/render/find_replace_menu/search_service.dart +++ b/lib/src/render/find_replace_menu/search_service.dart @@ -103,7 +103,7 @@ class SearchService { //unhighlight the selected word before it is replaced formatHighlight( editorState, - editorState.editorStyle.highlightColorHex!, + '0x6000BCF0', ); editorState.undoManager.forgetRecentUndo(); @@ -154,7 +154,7 @@ class SearchService { formatHighlight( editorState, - editorState.editorStyle.highlightColorHex!, + editorState.editorStyle.findHighlightColorHex!, ); editorState.undoManager.forgetRecentUndo(); } diff --git a/lib/src/render/style/editor_style.dart b/lib/src/render/style/editor_style.dart index b35102c36..2e7d15612 100644 --- a/lib/src/render/style/editor_style.dart +++ b/lib/src/render/style/editor_style.dart @@ -42,6 +42,7 @@ class EditorStyle extends ThemeExtension { final TextStyle? href; final TextStyle? code; final String? highlightColorHex; + final String? findHighlightColorHex; // Item's pop up menu styles final Color? popupMenuFGColor; @@ -70,6 +71,7 @@ class EditorStyle extends ThemeExtension { required this.href, required this.code, required this.highlightColorHex, + required this.findHighlightColorHex, required this.lineHeight, required this.popupMenuFGColor, required this.popupMenuHoverColor, @@ -98,6 +100,7 @@ class EditorStyle extends ThemeExtension { TextStyle? href, TextStyle? code, String? highlightColorHex, + String? findHighlightColorHex, double? lineHeight, Color? popupMenuFGColor, Color? popupMenuHoverColor, @@ -131,6 +134,8 @@ class EditorStyle extends ThemeExtension { href: href ?? this.href, code: code ?? this.code, highlightColorHex: highlightColorHex ?? this.highlightColorHex, + findHighlightColorHex: + findHighlightColorHex ?? this.findHighlightColorHex, lineHeight: lineHeight ?? this.lineHeight, popupMenuFGColor: popupMenuFGColor ?? this.popupMenuFGColor, popupMenuHoverColor: popupMenuHoverColor ?? this.popupMenuHoverColor, @@ -193,6 +198,7 @@ class EditorStyle extends ThemeExtension { href: TextStyle.lerp(href, other.href, t), code: TextStyle.lerp(code, other.code, t), highlightColorHex: highlightColorHex, + findHighlightColorHex: findHighlightColorHex, lineHeight: lineHeight, popupMenuFGColor: Color.lerp(popupMenuFGColor, other.popupMenuFGColor, t), popupMenuHoverColor: @@ -236,6 +242,7 @@ class EditorStyle extends ThemeExtension { backgroundColor: Color(0xFFE0F8FF), ), highlightColorHex: '0x6000BCF0', + findHighlightColorHex: '0x60F09E00', lineHeight: 1.5, popupMenuFGColor: const Color(0xFF333333), popupMenuHoverColor: const Color(0xFFE0F8FF), From 2b08ebbbd47d88e490cd21eb0e242279e2933a18 Mon Sep 17 00:00:00 2001 From: Mayur Mahajan Date: Sun, 21 May 2023 12:17:00 +0530 Subject: [PATCH 18/45] refactor: name of the search algo class --- lib/src/render/find_replace_menu/search_algorithm.dart | 2 +- lib/src/render/find_replace_menu/search_service.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/render/find_replace_menu/search_algorithm.dart b/lib/src/render/find_replace_menu/search_algorithm.dart index 8e468aac8..7ec0c766e 100644 --- a/lib/src/render/find_replace_menu/search_algorithm.dart +++ b/lib/src/render/find_replace_menu/search_algorithm.dart @@ -1,6 +1,6 @@ import 'dart:math' as math; -class BayerMooreAlgorithm { +class SearchAlgorithm { //This is a standard algorithm used for searching patterns in long text samples //It is more efficient than brute force searching because it is able to skip //characters that will never possibly match with required pattern. diff --git a/lib/src/render/find_replace_menu/search_service.dart b/lib/src/render/find_replace_menu/search_service.dart index 61bb2bd8b..6e51fe29a 100644 --- a/lib/src/render/find_replace_menu/search_service.dart +++ b/lib/src/render/find_replace_menu/search_service.dart @@ -11,7 +11,7 @@ class SearchService { //the position here consists of the node and the starting offset of the //matched pattern. We will use this to traverse between the matched patterns. List matchedPositions = []; - BayerMooreAlgorithm searchAlgorithm = BayerMooreAlgorithm(); + SearchAlgorithm searchAlgorithm = SearchAlgorithm(); String queriedPattern = ''; int selectedIndex = 0; From 5417668ca3a43e590a5a164fca3b9fa93417dba8 Mon Sep 17 00:00:00 2001 From: Mayur Mahajan Date: Sun, 21 May 2023 12:17:15 +0530 Subject: [PATCH 19/45] test: unit tests for search algorithm --- .../search_algorithm_test.dart | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 test/render/find_replace_menu/search_algorithm_test.dart diff --git a/test/render/find_replace_menu/search_algorithm_test.dart b/test/render/find_replace_menu/search_algorithm_test.dart new file mode 100644 index 000000000..3baa1d3ac --- /dev/null +++ b/test/render/find_replace_menu/search_algorithm_test.dart @@ -0,0 +1,69 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:appflowy_editor/src/render/find_replace_menu/search_algorithm.dart'; + +void main() { + group('SearchAlgorithm', () { + late SearchAlgorithm searchAlgorithm; + + setUpAll(() { + searchAlgorithm = SearchAlgorithm(); + }); + + test('searchAlgorithm returns the index of the only found pattern', () { + const pattern = 'Appflowy'; + const text = 'Welcome to Appflowy 😁'; + + List result = searchAlgorithm.boyerMooreSearch(pattern, text); + expect(result, [11]); + }); + + test('searchAlgorithm returns the index of the multiple found patterns', + () { + const pattern = 'Appflowy'; + const text = ''' +Welcome to Appflowy 😁. Appflowy is an open-source alternative to Notion. +With Appflowy, you can build detailed lists of to-do for different +projects while tracking the status of each one. With Appflowy, you can +visualize items in a database moving through stages of a process, or +grouped by property. Design and modify Appflowy your way with an +open core codebase. Appflowy is built with Flutter and Rust. + '''; + + List result = searchAlgorithm.boyerMooreSearch(pattern, text); + expect(result, [11, 24, 80, 196, 324, 371]); + }); + + test('searchAlgorithm returns empty list if pattern is not found', () { + const pattern = 'Flutter'; + const text = 'Welcome to Appflowy 😁'; + + final result = searchAlgorithm.boyerMooreSearch(pattern, text); + + expect(result, []); + }); + + test('searchAlgorithm returns pattern index if pattern is non-ASCII', () { + const pattern = '😁'; + const text = 'Welcome to Appflowy 😁'; + + List result = searchAlgorithm.boyerMooreSearch(pattern, text); + expect(result, [20]); + }); + + test('searchAlgorithm returns pattern index if pattern is not separate word', () { + const pattern = 'App'; + const text = 'Welcome to Appflowy 😁'; + + List result = searchAlgorithm.boyerMooreSearch(pattern, text); + expect(result, [11]); + }); + + test('searchAlgorithm returns empty list bcz it is case sensitive', () { + const pattern = 'APPFLOWY'; + const text = 'Welcome to Appflowy 😁'; + + List result = searchAlgorithm.boyerMooreSearch(pattern, text); + expect(result, []); + }); + }); +} From 190cf77d82493719ba9fbfa0f25287c03fea0bd7 Mon Sep 17 00:00:00 2001 From: Mayur Mahajan Date: Sun, 21 May 2023 13:44:19 +0530 Subject: [PATCH 20/45] chore: simplify syntax --- .../find_replace_menu/find_replace_widget.dart | 8 ++------ .../find_replace_menu/search_algorithm_test.dart | 12 ++++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/lib/src/render/find_replace_menu/find_replace_widget.dart b/lib/src/render/find_replace_menu/find_replace_widget.dart index d5b44ab22..803868930 100644 --- a/lib/src/render/find_replace_menu/find_replace_widget.dart +++ b/lib/src/render/find_replace_menu/find_replace_widget.dart @@ -82,9 +82,7 @@ class _FindMenuWidgetState extends State { onPressed: () { widget.dismiss(); searchService.findAndHighlight(queriedPattern); - setState(() { - queriedPattern = ''; - }); + setState(() => queriedPattern = ''); }, icon: const Icon(Icons.close), tooltip: 'Close', @@ -131,9 +129,7 @@ class _FindMenuWidgetState extends State { void _searchPattern() { searchService.findAndHighlight(findController.text); - setState(() { - queriedPattern = findController.text; - }); + setState(() => queriedPattern = findController.text); } void _replaceSelectedWord() { diff --git a/test/render/find_replace_menu/search_algorithm_test.dart b/test/render/find_replace_menu/search_algorithm_test.dart index 3baa1d3ac..e4ca387a6 100644 --- a/test/render/find_replace_menu/search_algorithm_test.dart +++ b/test/render/find_replace_menu/search_algorithm_test.dart @@ -9,7 +9,7 @@ void main() { searchAlgorithm = SearchAlgorithm(); }); - test('searchAlgorithm returns the index of the only found pattern', () { + test('returns the index of the only found pattern', () { const pattern = 'Appflowy'; const text = 'Welcome to Appflowy 😁'; @@ -17,7 +17,7 @@ void main() { expect(result, [11]); }); - test('searchAlgorithm returns the index of the multiple found patterns', + test('returns the index of the multiple found patterns', () { const pattern = 'Appflowy'; const text = ''' @@ -33,7 +33,7 @@ open core codebase. Appflowy is built with Flutter and Rust. expect(result, [11, 24, 80, 196, 324, 371]); }); - test('searchAlgorithm returns empty list if pattern is not found', () { + test('returns empty list if pattern is not found', () { const pattern = 'Flutter'; const text = 'Welcome to Appflowy 😁'; @@ -42,7 +42,7 @@ open core codebase. Appflowy is built with Flutter and Rust. expect(result, []); }); - test('searchAlgorithm returns pattern index if pattern is non-ASCII', () { + test('returns pattern index if pattern is non-ASCII', () { const pattern = '😁'; const text = 'Welcome to Appflowy 😁'; @@ -50,7 +50,7 @@ open core codebase. Appflowy is built with Flutter and Rust. expect(result, [20]); }); - test('searchAlgorithm returns pattern index if pattern is not separate word', () { + test('returns pattern index if pattern is not separate word', () { const pattern = 'App'; const text = 'Welcome to Appflowy 😁'; @@ -58,7 +58,7 @@ open core codebase. Appflowy is built with Flutter and Rust. expect(result, [11]); }); - test('searchAlgorithm returns empty list bcz it is case sensitive', () { + test('returns empty list bcz it is case sensitive', () { const pattern = 'APPFLOWY'; const text = 'Welcome to Appflowy 😁'; From 7dbb7b5c16217a5aacfc151fd1a16a0d37084afb Mon Sep 17 00:00:00 2001 From: Mayur Mahajan Date: Sun, 21 May 2023 13:55:45 +0530 Subject: [PATCH 21/45] test: renamed test group --- .../search_algorithm_test.dart | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/test/render/find_replace_menu/search_algorithm_test.dart b/test/render/find_replace_menu/search_algorithm_test.dart index e4ca387a6..5b094ae34 100644 --- a/test/render/find_replace_menu/search_algorithm_test.dart +++ b/test/render/find_replace_menu/search_algorithm_test.dart @@ -2,14 +2,14 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:appflowy_editor/src/render/find_replace_menu/search_algorithm.dart'; void main() { - group('SearchAlgorithm', () { + group('search_algorithm_test.dart', () { late SearchAlgorithm searchAlgorithm; - setUpAll(() { + setUp(() { searchAlgorithm = SearchAlgorithm(); }); - test('returns the index of the only found pattern', () { + test('search algorithm returns the index of the only found pattern', () { const pattern = 'Appflowy'; const text = 'Welcome to Appflowy 😁'; @@ -17,7 +17,7 @@ void main() { expect(result, [11]); }); - test('returns the index of the multiple found patterns', + test('search algorithm returns the index of the multiple found patterns', () { const pattern = 'Appflowy'; const text = ''' @@ -33,7 +33,7 @@ open core codebase. Appflowy is built with Flutter and Rust. expect(result, [11, 24, 80, 196, 324, 371]); }); - test('returns empty list if pattern is not found', () { + test('search algorithm returns empty list if pattern is not found', () { const pattern = 'Flutter'; const text = 'Welcome to Appflowy 😁'; @@ -42,7 +42,7 @@ open core codebase. Appflowy is built with Flutter and Rust. expect(result, []); }); - test('returns pattern index if pattern is non-ASCII', () { + test('search algorithm returns pattern index if pattern is non-ASCII', () { const pattern = '😁'; const text = 'Welcome to Appflowy 😁'; @@ -50,7 +50,9 @@ open core codebase. Appflowy is built with Flutter and Rust. expect(result, [20]); }); - test('returns pattern index if pattern is not separate word', () { + test( + 'search algorithm returns pattern index if pattern is not separate word', + () { const pattern = 'App'; const text = 'Welcome to Appflowy 😁'; @@ -58,7 +60,7 @@ open core codebase. Appflowy is built with Flutter and Rust. expect(result, [11]); }); - test('returns empty list bcz it is case sensitive', () { + test('search algorithm returns empty list bcz it is case sensitive', () { const pattern = 'APPFLOWY'; const text = 'Welcome to Appflowy 😁'; From 47194cfcf483d84771d4e0e5fb9edacd4b8adff3 Mon Sep 17 00:00:00 2001 From: MayurSMahajan Date: Mon, 17 Jul 2023 20:10:17 +0530 Subject: [PATCH 22/45] refactor: move to editor --- .../find_replace_menu/find_menu_service.dart | 5 +- .../find_replace_widget.dart | 2 +- .../find_replace_menu/search_algorithm.dart | 0 .../find_replace_menu/search_service.dart | 10 +- .../find_replace_handler.dart | 60 -- .../find_replace_menu_find_test.dart | 818 +++++++++--------- .../find_replace_menu_replace_test.dart | 484 +++++------ .../search_algorithm_test.dart | 2 +- 8 files changed, 660 insertions(+), 721 deletions(-) rename lib/src/{render => editor}/find_replace_menu/find_menu_service.dart (94%) rename lib/src/{render => editor}/find_replace_menu/find_replace_widget.dart (98%) rename lib/src/{render => editor}/find_replace_menu/search_algorithm.dart (100%) rename lib/src/{render => editor}/find_replace_menu/search_service.dart (95%) delete mode 100644 lib/src/service/internal_key_event_handlers/find_replace_handler.dart diff --git a/lib/src/render/find_replace_menu/find_menu_service.dart b/lib/src/editor/find_replace_menu/find_menu_service.dart similarity index 94% rename from lib/src/render/find_replace_menu/find_menu_service.dart rename to lib/src/editor/find_replace_menu/find_menu_service.dart index 0fa1bfe97..06e9fc4e4 100644 --- a/lib/src/render/find_replace_menu/find_menu_service.dart +++ b/lib/src/editor/find_replace_menu/find_menu_service.dart @@ -1,5 +1,5 @@ import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor/src/render/find_replace_menu/find_replace_widget.dart'; +import 'package:appflowy_editor/src/editor/find_replace_menu/find_replace_widget.dart'; import 'package:flutter/material.dart'; import '../../editor_state.dart'; @@ -61,8 +61,7 @@ class FindReplaceMenu implements FindReplaceService { borderRadius: BorderRadius.circular(8.0), child: DecoratedBox( decoration: BoxDecoration( - color: editorState.editorStyle.selectionMenuBackgroundColor ?? - Colors.white, + color: editorState.editorStyle.selectionColor, boxShadow: [ BoxShadow( blurRadius: 5, diff --git a/lib/src/render/find_replace_menu/find_replace_widget.dart b/lib/src/editor/find_replace_menu/find_replace_widget.dart similarity index 98% rename from lib/src/render/find_replace_menu/find_replace_widget.dart rename to lib/src/editor/find_replace_menu/find_replace_widget.dart index 803868930..4cc8758c7 100644 --- a/lib/src/render/find_replace_menu/find_replace_widget.dart +++ b/lib/src/editor/find_replace_menu/find_replace_widget.dart @@ -1,5 +1,5 @@ import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor/src/render/find_replace_menu/search_service.dart'; +import 'package:appflowy_editor/src/editor/find_replace_menu/search_service.dart'; import 'package:flutter/material.dart'; class FindMenuWidget extends StatefulWidget { diff --git a/lib/src/render/find_replace_menu/search_algorithm.dart b/lib/src/editor/find_replace_menu/search_algorithm.dart similarity index 100% rename from lib/src/render/find_replace_menu/search_algorithm.dart rename to lib/src/editor/find_replace_menu/search_algorithm.dart diff --git a/lib/src/render/find_replace_menu/search_service.dart b/lib/src/editor/find_replace_menu/search_service.dart similarity index 95% rename from lib/src/render/find_replace_menu/search_service.dart rename to lib/src/editor/find_replace_menu/search_service.dart index 6e51fe29a..7b17dcd46 100644 --- a/lib/src/render/find_replace_menu/search_service.dart +++ b/lib/src/editor/find_replace_menu/search_service.dart @@ -1,5 +1,5 @@ import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor/src/render/find_replace_menu/search_algorithm.dart'; +import 'package:appflowy_editor/src/editor/find_replace_menu/search_algorithm.dart'; class SearchService { SearchService({ @@ -101,7 +101,7 @@ class SearchService { _selectWordAtPosition(matchedPosition); //unhighlight the selected word before it is replaced - formatHighlight( + formatHighlightColor( editorState, '0x6000BCF0', ); @@ -152,9 +152,9 @@ class SearchService { Position start = Position(path: path, offset: match); _selectWordAtPosition(start); - formatHighlight( + formatHighlightColor( editorState, - editorState.editorStyle.findHighlightColorHex!, + '0x6000BCF0', ); editorState.undoManager.forgetRecentUndo(); } @@ -166,6 +166,6 @@ class SearchService { offset: start.offset + queriedPattern.length, ); - editorState.updateCursorSelection(Selection(start: start, end: end)); + editorState.updateSelectionWithReason(Selection(start: start, end: end)); } } diff --git a/lib/src/service/internal_key_event_handlers/find_replace_handler.dart b/lib/src/service/internal_key_event_handlers/find_replace_handler.dart deleted file mode 100644 index 345d51a37..000000000 --- a/lib/src/service/internal_key_event_handlers/find_replace_handler.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:appflowy_editor/src/core/document/node.dart'; -import 'package:appflowy_editor/src/extensions/node_extensions.dart'; -import 'package:appflowy_editor/src/render/find_replace_menu/find_menu_service.dart'; -import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart'; -import 'package:flutter/material.dart'; - -FindReplaceService? _findMenuService; -ShortcutEventHandler findShortcutHandler = (editorState, event) { - final textNodes = editorState.service.selectionService.currentSelectedNodes - .whereType(); - if (textNodes.length != 1) { - return KeyEventResult.ignored; - } - - final selection = editorState.service.selectionService.currentSelection.value; - final textNode = textNodes.first; - final context = textNode.context; - final selectable = textNode.selectable; - if (selection == null || context == null || selectable == null) { - return KeyEventResult.ignored; - } - - WidgetsBinding.instance.addPostFrameCallback((_) { - _findMenuService = FindReplaceMenu( - context: context, - editorState: editorState, - replaceFlag: false, - ); - _findMenuService?.show(); - }); - - return KeyEventResult.handled; -}; - -ShortcutEventHandler replaceShortcutHandler = (editorState, event) { - final textNodes = editorState.service.selectionService.currentSelectedNodes - .whereType(); - if (textNodes.length != 1) { - return KeyEventResult.ignored; - } - - final selection = editorState.service.selectionService.currentSelection.value; - final textNode = textNodes.first; - final context = textNode.context; - final selectable = textNode.selectable; - if (selection == null || context == null || selectable == null) { - return KeyEventResult.ignored; - } - - WidgetsBinding.instance.addPostFrameCallback((_) { - _findMenuService = FindReplaceMenu( - context: context, - editorState: editorState, - replaceFlag: true, - ); - _findMenuService?.show(); - }); - - return KeyEventResult.handled; -}; diff --git a/test/render/find_replace_menu/find_replace_menu_find_test.dart b/test/render/find_replace_menu/find_replace_menu_find_test.dart index 92276b17e..d6f6a8fce 100644 --- a/test/render/find_replace_menu/find_replace_menu_find_test.dart +++ b/test/render/find_replace_menu/find_replace_menu_find_test.dart @@ -1,409 +1,409 @@ -import 'dart:io'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:appflowy_editor/src/render/find_replace_menu/find_replace_widget.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import '../../infra/test_editor.dart'; - -void main() async { - setUpAll(() { - TestWidgetsFlutterBinding.ensureInitialized(); - }); - - group('find_replace_menu.dart findMenu', () { - testWidgets('find menu appears properly', (tester) async { - await _prepare(tester, lines: 3); - - //the prepare method only checks if FindMenuWidget is present - //so here we also check if FindMenuWidget contains TextField - //and IconButtons or not. - expect(find.byType(TextField), findsOneWidget); - expect(find.byType(IconButton), findsAtLeastNWidgets(4)); - }); - - testWidgets('find menu disappears when close is called', (tester) async { - await _prepare(tester, lines: 3); - - //lets check if find menu disappears if the close button is tapped. - await tester.tap(find.byKey(const Key('closeButton'))); - await tester.pumpAndSettle(); - - expect(find.byType(FindMenuWidget), findsNothing); - expect(find.byType(TextField), findsNothing); - expect(find.byType(IconButton), findsNothing); - }); - - testWidgets('find menu does not work with empty input', (tester) async { - const pattern = ''; - - //we are passing empty string for pattern - final editor = await _prepareWithTextInputForFind( - tester, - lines: 1, - pattern: pattern, - ); - - //since the method will not select anything as searched pattern is - //empty, the current selection should be equal to previous selection. - final selection = - editor.editorState.service.selectionService.currentSelection.value; - - expect(selection, Selection.single(path: [0], startOffset: 0)); - - //we can do this because there is only one text node. - final textNode = editor.nodeAtPath([0]) as TextNode; - - //we expect that nothing is highlighted in our current document. - expect( - textNode.allSatisfyInSelection( - selection!, - BuiltInAttributeKey.backgroundColor, - (value) => value == '0x00000000', - ), - true, - ); - }); - - testWidgets('find menu works properly when match is not found', - (tester) async { - const pattern = 'Flutter'; - - final editor = await _prepareWithTextInputForFind( - tester, - lines: 1, - pattern: pattern, - ); - - //fetching the current selection - final selection = - editor.editorState.service.selectionService.currentSelection.value; - - //since no match is found the current selection should not be different - //from initial selection. - expect(selection != null, true); - expect(selection, Selection.single(path: [0], startOffset: 0)); - }); - - testWidgets('found matches are highlighted', (tester) async { - const pattern = 'Welcome'; - - final editor = await _prepareWithTextInputForFind( - tester, - lines: 3, - pattern: pattern, - ); - - //checking if current selection consists an occurance of matched pattern. - final selection = - editor.editorState.service.selectionService.currentSelection.value; - - //we expect the last occurance of the pattern to be found, thus that should - //be the current selection. - expect(selection != null, true); - expect(selection!.start, Position(path: [2], offset: 0)); - expect(selection.end, Position(path: [2], offset: pattern.length)); - - //check whether the node with found occurance of patten is highlighted - final textNode = editor.nodeAtPath([2]) as TextNode; - - //we expect that the current selected node is highlighted. - //we can confirm that by saying that the node's backgroung color is not white. - expect( - textNode.allSatisfyInSelection( - selection, - BuiltInAttributeKey.backgroundColor, - (value) => value != '0x00000000', - ), - true, - ); - }); - - testWidgets('navigating to previous matches works', (tester) async { - const pattern = 'Welcome'; - const previousBtnKey = Key('previousMatchButton'); - - final editor = await _prepareWithTextInputForFind( - tester, - lines: 2, - pattern: pattern, - ); - - //checking if current selection consists an occurance of matched pattern. - var selection = - editor.editorState.service.selectionService.currentSelection.value; - - //we expect the last occurance of the pattern to be found, thus that should - //be the current selection. - expect(selection != null, true); - expect(selection!.start, Position(path: [1], offset: 0)); - expect(selection.end, Position(path: [1], offset: pattern.length)); - - //now pressing the icon button for previous match should select - //node at path [0]. - await tester.tap(find.byKey(previousBtnKey)); - await tester.pumpAndSettle(); - - selection = - editor.editorState.service.selectionService.currentSelection.value; - - expect(selection != null, true); - expect(selection!.start, Position(path: [0], offset: 0)); - expect(selection.end, Position(path: [0], offset: pattern.length)); - - //now pressing the icon button for previous match should select - //node at path [1], since there is no node before node at [0]. - await tester.tap(find.byKey(previousBtnKey)); - await tester.pumpAndSettle(); - - selection = - editor.editorState.service.selectionService.currentSelection.value; - - expect(selection != null, true); - expect(selection!.start, Position(path: [1], offset: 0)); - expect(selection.end, Position(path: [1], offset: pattern.length)); - }); - - testWidgets('navigating to next matches works', (tester) async { - const pattern = 'Welcome'; - const nextBtnKey = Key('nextMatchButton'); - - final editor = await _prepareWithTextInputForFind( - tester, - lines: 3, - pattern: pattern, - ); - - //the last found occurance should be selected - var selection = - editor.editorState.service.selectionService.currentSelection.value; - - expect(selection != null, true); - expect(selection!.start, Position(path: [2], offset: 0)); - expect(selection.end, Position(path: [2], offset: pattern.length)); - - //now pressing the icon button for next match should select - //node at path [0], since there are no nodes after node at [2]. - await tester.tap(find.byKey(nextBtnKey)); - await tester.pumpAndSettle(); - - selection = - editor.editorState.service.selectionService.currentSelection.value; - - expect(selection != null, true); - expect(selection!.start, Position(path: [0], offset: 0)); - expect(selection.end, Position(path: [0], offset: pattern.length)); - - //now pressing the icon button for previous match should select - //node at path [1]. - await tester.tap(find.byKey(nextBtnKey)); - await tester.pumpAndSettle(); - - selection = - editor.editorState.service.selectionService.currentSelection.value; - - expect(selection != null, true); - expect(selection!.start, Position(path: [1], offset: 0)); - expect(selection.end, Position(path: [1], offset: pattern.length)); - }); - - testWidgets('found matches are unhighlighted when findMenu closed', - (tester) async { - const pattern = 'Welcome'; - const closeBtnKey = Key('closeButton'); - - final editor = await _prepareWithTextInputForFind( - tester, - lines: 3, - pattern: pattern, - ); - - final selection = - editor.editorState.service.selectionService.currentSelection.value; - - final textNode = editor.nodeAtPath([2]) as TextNode; - - //node is highlighted while menu is active - expect( - textNode.allSatisfyInSelection( - selection!, - BuiltInAttributeKey.backgroundColor, - (value) => value != '0x00000000', - ), - true, - ); - - //presses the close button - await tester.tap(find.byKey(closeBtnKey)); - await tester.pumpAndSettle(); - - //closes the findMenuWidget - expect(find.byType(FindMenuWidget), findsNothing); - - //node is unhighlighted after the menu is closed - expect( - textNode.allSatisfyInSelection( - selection, - BuiltInAttributeKey.backgroundColor, - (value) => value == '0x00000000', - ), - true, - ); - }); - - testWidgets('old matches are unhighlighted when new pattern is searched', - (tester) async { - const textInputKey = Key('findTextField'); - - const textLine1 = 'Welcome to Appflowy 😁'; - const textLine2 = 'Appflowy is made with Flutter, Rust and ❤️'; - var pattern = 'Welcome'; - - final editor = tester.editor - ..insertTextNode(textLine1) - ..insertTextNode(textLine2); - - await editor.startTesting(); - await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); - - if (Platform.isWindows || Platform.isLinux) { - await editor.pressLogicKey( - key: LogicalKeyboardKey.keyF, - isControlPressed: true, - ); - } else { - await editor.pressLogicKey( - key: LogicalKeyboardKey.keyF, - isMetaPressed: true, - ); - } - - await tester.pumpAndSettle(const Duration(milliseconds: 1000)); - - await tester.tap(find.byKey(textInputKey)); - await tester.enterText(find.byKey(textInputKey), pattern); - await tester.pumpAndSettle(); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - - //finds the pattern - await editor.pressLogicKey( - key: LogicalKeyboardKey.enter, - ); - - //since node at path [1] does not contain match, we expect it - //to be not highlighted. - var selection = Selection.single(path: [1], startOffset: 0); - var textNode = editor.nodeAtPath([1]) as TextNode; - - expect( - textNode.allSatisfyInSelection( - selection, - BuiltInAttributeKey.backgroundColor, - (value) => value == '0x00000000', - ), - true, - ); - - //now we will change the pattern to Flutter and search it - pattern = 'Flutter'; - await tester.tap(find.byKey(textInputKey)); - await tester.enterText(find.byKey(textInputKey), pattern); - await tester.pumpAndSettle(); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - - //finds the pattern Flutter - await editor.pressLogicKey( - key: LogicalKeyboardKey.enter, - ); - - //now we expect the text node at path 1 to contain highlighted pattern - expect( - textNode.allSatisfyInSelection( - selection, - BuiltInAttributeKey.backgroundColor, - (value) => value != '0x00000000', - ), - true, - ); - }); - }); -} - -Future _prepare( - WidgetTester tester, { - int lines = 1, -}) async { - const text = 'Welcome to Appflowy 😁'; - final editor = tester.editor; - for (var i = 0; i < lines; i++) { - editor.insertTextNode(text); - } - await editor.startTesting(); - await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); - - if (Platform.isWindows || Platform.isLinux) { - await editor.pressLogicKey( - key: LogicalKeyboardKey.keyF, - isControlPressed: true, - ); - } else { - await editor.pressLogicKey( - key: LogicalKeyboardKey.keyF, - isMetaPressed: true, - ); - } - - await tester.pumpAndSettle(const Duration(milliseconds: 1000)); - - expect(find.byType(FindMenuWidget), findsOneWidget); - - return Future.value(editor); -} - -Future _prepareWithTextInputForFind( - WidgetTester tester, { - int lines = 1, - String pattern = "Welcome", -}) async { - const text = 'Welcome to Appflowy 😁'; - const textInputKey = Key('findTextField'); - final editor = tester.editor; - for (var i = 0; i < lines; i++) { - editor.insertTextNode(text); - } - await editor.startTesting(); - await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); - - if (Platform.isWindows || Platform.isLinux) { - await editor.pressLogicKey( - key: LogicalKeyboardKey.keyF, - isControlPressed: true, - ); - } else { - await editor.pressLogicKey( - key: LogicalKeyboardKey.keyF, - isMetaPressed: true, - ); - } - - await tester.pumpAndSettle(const Duration(milliseconds: 1000)); - - expect(find.byType(FindMenuWidget), findsOneWidget); - - await tester.tap(find.byKey(textInputKey)); - await tester.enterText(find.byKey(textInputKey), pattern); - await tester.pumpAndSettle(); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - - //pressing enter should trigger the findAndHighlight method, which - //will find the pattern inside the editor. - await editor.pressLogicKey( - key: LogicalKeyboardKey.enter, - ); - - return Future.value(editor); -} +// import 'dart:io'; +// import 'package:flutter/material.dart'; +// import 'package:flutter/services.dart'; +// import 'package:flutter_test/flutter_test.dart'; + +// import 'package:appflowy_editor/src/editor/find_replace_menu/find_replace_widget.dart'; +// import 'package:appflowy_editor/appflowy_editor.dart'; +// import '../../infra/test_editor.dart'; + +// void main() async { +// setUpAll(() { +// TestWidgetsFlutterBinding.ensureInitialized(); +// }); + +// group('find_replace_menu.dart findMenu', () { +// testWidgets('find menu appears properly', (tester) async { +// await _prepare(tester, lines: 3); + +// //the prepare method only checks if FindMenuWidget is present +// //so here we also check if FindMenuWidget contains TextField +// //and IconButtons or not. +// expect(find.byType(TextField), findsOneWidget); +// expect(find.byType(IconButton), findsAtLeastNWidgets(4)); +// }); + +// testWidgets('find menu disappears when close is called', (tester) async { +// await _prepare(tester, lines: 3); + +// //lets check if find menu disappears if the close button is tapped. +// await tester.tap(find.byKey(const Key('closeButton'))); +// await tester.pumpAndSettle(); + +// expect(find.byType(FindMenuWidget), findsNothing); +// expect(find.byType(TextField), findsNothing); +// expect(find.byType(IconButton), findsNothing); +// }); + +// testWidgets('find menu does not work with empty input', (tester) async { +// const pattern = ''; + +// //we are passing empty string for pattern +// final editor = await _prepareWithTextInputForFind( +// tester, +// lines: 1, +// pattern: pattern, +// ); + +// //since the method will not select anything as searched pattern is +// //empty, the current selection should be equal to previous selection. +// final selection = +// editor.editorState.service.selectionService.currentSelection.value; + +// expect(selection, Selection.single(path: [0], startOffset: 0)); + +// //we can do this because there is only one text node. +// final textNode = editor.nodeAtPath([0]) as TextNode; + +// //we expect that nothing is highlighted in our current document. +// expect( +// textNode.allSatisfyInSelection( +// selection!, +// BuiltInAttributeKey.backgroundColor, +// (value) => value == '0x00000000', +// ), +// true, +// ); +// }); + +// testWidgets('find menu works properly when match is not found', +// (tester) async { +// const pattern = 'Flutter'; + +// final editor = await _prepareWithTextInputForFind( +// tester, +// lines: 1, +// pattern: pattern, +// ); + +// //fetching the current selection +// final selection = +// editor.editorState.service.selectionService.currentSelection.value; + +// //since no match is found the current selection should not be different +// //from initial selection. +// expect(selection != null, true); +// expect(selection, Selection.single(path: [0], startOffset: 0)); +// }); + +// testWidgets('found matches are highlighted', (tester) async { +// const pattern = 'Welcome'; + +// final editor = await _prepareWithTextInputForFind( +// tester, +// lines: 3, +// pattern: pattern, +// ); + +// //checking if current selection consists an occurance of matched pattern. +// final selection = +// editor.editorState.service.selectionService.currentSelection.value; + +// //we expect the last occurance of the pattern to be found, thus that should +// //be the current selection. +// expect(selection != null, true); +// expect(selection!.start, Position(path: [2], offset: 0)); +// expect(selection.end, Position(path: [2], offset: pattern.length)); + +// //check whether the node with found occurance of patten is highlighted +// final textNode = editor.nodeAtPath([2]) as TextNode; + +// //we expect that the current selected node is highlighted. +// //we can confirm that by saying that the node's backgroung color is not white. +// expect( +// textNode.allSatisfyInSelection( +// selection, +// BuiltInAttributeKey.backgroundColor, +// (value) => value != '0x00000000', +// ), +// true, +// ); +// }); + +// testWidgets('navigating to previous matches works', (tester) async { +// const pattern = 'Welcome'; +// const previousBtnKey = Key('previousMatchButton'); + +// final editor = await _prepareWithTextInputForFind( +// tester, +// lines: 2, +// pattern: pattern, +// ); + +// //checking if current selection consists an occurance of matched pattern. +// var selection = +// editor.editorState.service.selectionService.currentSelection.value; + +// //we expect the last occurance of the pattern to be found, thus that should +// //be the current selection. +// expect(selection != null, true); +// expect(selection!.start, Position(path: [1], offset: 0)); +// expect(selection.end, Position(path: [1], offset: pattern.length)); + +// //now pressing the icon button for previous match should select +// //node at path [0]. +// await tester.tap(find.byKey(previousBtnKey)); +// await tester.pumpAndSettle(); + +// selection = +// editor.editorState.service.selectionService.currentSelection.value; + +// expect(selection != null, true); +// expect(selection!.start, Position(path: [0], offset: 0)); +// expect(selection.end, Position(path: [0], offset: pattern.length)); + +// //now pressing the icon button for previous match should select +// //node at path [1], since there is no node before node at [0]. +// await tester.tap(find.byKey(previousBtnKey)); +// await tester.pumpAndSettle(); + +// selection = +// editor.editorState.service.selectionService.currentSelection.value; + +// expect(selection != null, true); +// expect(selection!.start, Position(path: [1], offset: 0)); +// expect(selection.end, Position(path: [1], offset: pattern.length)); +// }); + +// testWidgets('navigating to next matches works', (tester) async { +// const pattern = 'Welcome'; +// const nextBtnKey = Key('nextMatchButton'); + +// final editor = await _prepareWithTextInputForFind( +// tester, +// lines: 3, +// pattern: pattern, +// ); + +// //the last found occurance should be selected +// var selection = +// editor.editorState.service.selectionService.currentSelection.value; + +// expect(selection != null, true); +// expect(selection!.start, Position(path: [2], offset: 0)); +// expect(selection.end, Position(path: [2], offset: pattern.length)); + +// //now pressing the icon button for next match should select +// //node at path [0], since there are no nodes after node at [2]. +// await tester.tap(find.byKey(nextBtnKey)); +// await tester.pumpAndSettle(); + +// selection = +// editor.editorState.service.selectionService.currentSelection.value; + +// expect(selection != null, true); +// expect(selection!.start, Position(path: [0], offset: 0)); +// expect(selection.end, Position(path: [0], offset: pattern.length)); + +// //now pressing the icon button for previous match should select +// //node at path [1]. +// await tester.tap(find.byKey(nextBtnKey)); +// await tester.pumpAndSettle(); + +// selection = +// editor.editorState.service.selectionService.currentSelection.value; + +// expect(selection != null, true); +// expect(selection!.start, Position(path: [1], offset: 0)); +// expect(selection.end, Position(path: [1], offset: pattern.length)); +// }); + +// testWidgets('found matches are unhighlighted when findMenu closed', +// (tester) async { +// const pattern = 'Welcome'; +// const closeBtnKey = Key('closeButton'); + +// final editor = await _prepareWithTextInputForFind( +// tester, +// lines: 3, +// pattern: pattern, +// ); + +// final selection = +// editor.editorState.service.selectionService.currentSelection.value; + +// final textNode = editor.nodeAtPath([2]) as TextNode; + +// //node is highlighted while menu is active +// expect( +// textNode.allSatisfyInSelection( +// selection!, +// BuiltInAttributeKey.backgroundColor, +// (value) => value != '0x00000000', +// ), +// true, +// ); + +// //presses the close button +// await tester.tap(find.byKey(closeBtnKey)); +// await tester.pumpAndSettle(); + +// //closes the findMenuWidget +// expect(find.byType(FindMenuWidget), findsNothing); + +// //node is unhighlighted after the menu is closed +// expect( +// textNode.allSatisfyInSelection( +// selection, +// BuiltInAttributeKey.backgroundColor, +// (value) => value == '0x00000000', +// ), +// true, +// ); +// }); + +// testWidgets('old matches are unhighlighted when new pattern is searched', +// (tester) async { +// const textInputKey = Key('findTextField'); + +// const textLine1 = 'Welcome to Appflowy 😁'; +// const textLine2 = 'Appflowy is made with Flutter, Rust and ❤️'; +// var pattern = 'Welcome'; + +// final editor = tester.editor +// ..insertTextNode(textLine1) +// ..insertTextNode(textLine2); + +// await editor.startTesting(); +// await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + +// if (Platform.isWindows || Platform.isLinux) { +// await editor.pressLogicKey( +// key: LogicalKeyboardKey.keyF, +// isControlPressed: true, +// ); +// } else { +// await editor.pressLogicKey( +// key: LogicalKeyboardKey.keyF, +// isMetaPressed: true, +// ); +// } + +// await tester.pumpAndSettle(const Duration(milliseconds: 1000)); + +// await tester.tap(find.byKey(textInputKey)); +// await tester.enterText(find.byKey(textInputKey), pattern); +// await tester.pumpAndSettle(); +// await tester.testTextInput.receiveAction(TextInputAction.done); +// await tester.pumpAndSettle(); + +// //finds the pattern +// await editor.pressLogicKey( +// key: LogicalKeyboardKey.enter, +// ); + +// //since node at path [1] does not contain match, we expect it +// //to be not highlighted. +// var selection = Selection.single(path: [1], startOffset: 0); +// var textNode = editor.nodeAtPath([1]) as TextNode; + +// expect( +// textNode.allSatisfyInSelection( +// selection, +// BuiltInAttributeKey.backgroundColor, +// (value) => value == '0x00000000', +// ), +// true, +// ); + +// //now we will change the pattern to Flutter and search it +// pattern = 'Flutter'; +// await tester.tap(find.byKey(textInputKey)); +// await tester.enterText(find.byKey(textInputKey), pattern); +// await tester.pumpAndSettle(); +// await tester.testTextInput.receiveAction(TextInputAction.done); +// await tester.pumpAndSettle(); + +// //finds the pattern Flutter +// await editor.pressLogicKey( +// key: LogicalKeyboardKey.enter, +// ); + +// //now we expect the text node at path 1 to contain highlighted pattern +// expect( +// textNode.allSatisfyInSelection( +// selection, +// BuiltInAttributeKey.backgroundColor, +// (value) => value != '0x00000000', +// ), +// true, +// ); +// }); +// }); +// } + +// Future _prepare( +// WidgetTester tester, { +// int lines = 1, +// }) async { +// const text = 'Welcome to Appflowy 😁'; +// final editor = tester.editor; +// for (var i = 0; i < lines; i++) { +// editor.insertTextNode(text); +// } +// await editor.startTesting(); +// await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + +// if (Platform.isWindows || Platform.isLinux) { +// await editor.pressLogicKey( +// key: LogicalKeyboardKey.keyF, +// isControlPressed: true, +// ); +// } else { +// await editor.pressLogicKey( +// key: LogicalKeyboardKey.keyF, +// isMetaPressed: true, +// ); +// } + +// await tester.pumpAndSettle(const Duration(milliseconds: 1000)); + +// expect(find.byType(FindMenuWidget), findsOneWidget); + +// return Future.value(editor); +// } + +// Future _prepareWithTextInputForFind( +// WidgetTester tester, { +// int lines = 1, +// String pattern = "Welcome", +// }) async { +// const text = 'Welcome to Appflowy 😁'; +// const textInputKey = Key('findTextField'); +// final editor = tester.editor; +// for (var i = 0; i < lines; i++) { +// editor.insertTextNode(text); +// } +// await editor.startTesting(); +// await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + +// if (Platform.isWindows || Platform.isLinux) { +// await editor.pressLogicKey( +// key: LogicalKeyboardKey.keyF, +// isControlPressed: true, +// ); +// } else { +// await editor.pressLogicKey( +// key: LogicalKeyboardKey.keyF, +// isMetaPressed: true, +// ); +// } + +// await tester.pumpAndSettle(const Duration(milliseconds: 1000)); + +// expect(find.byType(FindMenuWidget), findsOneWidget); + +// await tester.tap(find.byKey(textInputKey)); +// await tester.enterText(find.byKey(textInputKey), pattern); +// await tester.pumpAndSettle(); +// await tester.testTextInput.receiveAction(TextInputAction.done); +// await tester.pumpAndSettle(); + +// //pressing enter should trigger the findAndHighlight method, which +// //will find the pattern inside the editor. +// await editor.pressLogicKey( +// key: LogicalKeyboardKey.enter, +// ); + +// return Future.value(editor); +// } diff --git a/test/render/find_replace_menu/find_replace_menu_replace_test.dart b/test/render/find_replace_menu/find_replace_menu_replace_test.dart index 98d14a94e..57f0e3094 100644 --- a/test/render/find_replace_menu/find_replace_menu_replace_test.dart +++ b/test/render/find_replace_menu/find_replace_menu_replace_test.dart @@ -1,242 +1,242 @@ -import 'dart:io'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:appflowy_editor/src/render/find_replace_menu/find_replace_widget.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import '../../infra/test_editor.dart'; - -void main() async { - setUpAll(() { - TestWidgetsFlutterBinding.ensureInitialized(); - }); - - group('find_replace_menu.dart replaceMenu', () { - testWidgets('replace menu appears properly', (tester) async { - await _prepare(tester, lines: 3); - - //the prepare method only checks if FindMenuWidget is present - //so here we also check if FindMenuWidget contains TextField - //and IconButtons or not. - //and whether there are two textfields for replace menu as well. - expect(find.byType(TextField), findsNWidgets(2)); - expect(find.byType(IconButton), findsAtLeastNWidgets(6)); - }); - - testWidgets('replace menu disappears when close is called', (tester) async { - await _prepare(tester, lines: 3); - - await tester.tap(find.byKey(const Key('closeButton'))); - await tester.pumpAndSettle(); - - expect(find.byType(FindMenuWidget), findsNothing); - expect(find.byType(TextField), findsNothing); - expect(find.byType(IconButton), findsNothing); - }); - - testWidgets('replace menu does not work when find is not called', - (tester) async { - const textInputKey = Key('replaceTextField'); - const pattern = 'Flutter'; - final editor = await _prepare(tester); - - await tester.tap(find.byKey(textInputKey)); - await tester.enterText(find.byKey(textInputKey), pattern); - await tester.pumpAndSettle(); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - - //pressing enter should trigger the replaceSelected method - await editor.pressLogicKey( - key: LogicalKeyboardKey.enter, - ); - await tester.pumpAndSettle(); - - //note our document only has one node - final textNode = editor.nodeAtPath([0]) as TextNode; - const expectedText = 'Welcome to Appflowy 😁'; - expect(textNode.toPlainText(), expectedText); - }); - - testWidgets('replace does not change text when no match is found', - (tester) async { - const textInputKey = Key('replaceTextField'); - const pattern = 'Flutter'; - - final editor = await _prepareWithTextInputForFind( - tester, - lines: 1, - pattern: pattern, - ); - - await tester.tap(find.byKey(textInputKey)); - await tester.enterText(find.byKey(textInputKey), pattern); - await tester.pumpAndSettle(); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - - await editor.pressLogicKey( - key: LogicalKeyboardKey.enter, - ); - await tester.pumpAndSettle(); - - final textNode = editor.nodeAtPath([0]) as TextNode; - const expectedText = 'Welcome to Appflowy 😁'; - expect(textNode.toPlainText(), expectedText); - }); - - testWidgets('found selected match is replaced properly', (tester) async { - const patternToBeFound = 'Welcome'; - const replacePattern = 'Salute'; - const textInputKey = Key('replaceTextField'); - - final editor = await _prepareWithTextInputForFind( - tester, - lines: 3, - pattern: patternToBeFound, - ); - - //check if matches are not yet replaced - var textNode = editor.nodeAtPath([2]) as TextNode; - var expectedText = '$patternToBeFound to Appflowy 😁'; - expect(textNode.toPlainText(), expectedText); - - //we select the replace text field and provide replacePattern - await tester.tap(find.byKey(textInputKey)); - await tester.enterText(find.byKey(textInputKey), replacePattern); - await tester.pumpAndSettle(); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - - await editor.pressLogicKey( - key: LogicalKeyboardKey.enter, - ); - - await tester.pumpAndSettle(); - - //we know that the findAndHighlight method selects the last - //matched occurance in the editor document. - textNode = editor.nodeAtPath([2]) as TextNode; - expectedText = '$replacePattern to Appflowy 😁'; - expect(textNode.toPlainText(), expectedText); - - //also check if other matches are not yet replaced - textNode = editor.nodeAtPath([1]) as TextNode; - expectedText = '$patternToBeFound to Appflowy 😁'; - expect(textNode.toPlainText(), expectedText); - }); - - testWidgets('replace all on found matches', (tester) async { - const patternToBeFound = 'Welcome'; - const replacePattern = 'Salute'; - const expectedText = '$replacePattern to Appflowy 😁'; - const lines = 3; - - const textInputKey = Key('replaceTextField'); - const replaceAllBtn = Key('replaceAllButton'); - - final editor = await _prepareWithTextInputForFind( - tester, - lines: lines, - pattern: patternToBeFound, - ); - - //check if matches are not yet replaced - var textNode = editor.nodeAtPath([2]) as TextNode; - var originalText = '$patternToBeFound to Appflowy 😁'; - expect(textNode.toPlainText(), originalText); - - await tester.tap(find.byKey(textInputKey)); - await tester.enterText(find.byKey(textInputKey), replacePattern); - await tester.pumpAndSettle(); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - - await tester.tap(find.byKey(replaceAllBtn)); - await tester.pumpAndSettle(); - - //all matches should be replaced - for (var i = 0; i < lines; i++) { - textNode = editor.nodeAtPath([i]) as TextNode; - expect(textNode.toPlainText(), expectedText); - } - }); - }); -} - -Future _prepare( - WidgetTester tester, { - int lines = 1, -}) async { - const text = 'Welcome to Appflowy 😁'; - final editor = tester.editor; - for (var i = 0; i < lines; i++) { - editor.insertTextNode(text); - } - await editor.startTesting(); - await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); - - if (Platform.isWindows || Platform.isLinux) { - await editor.pressLogicKey( - key: LogicalKeyboardKey.keyH, - isControlPressed: true, - ); - } else { - await editor.pressLogicKey( - key: LogicalKeyboardKey.keyH, - isMetaPressed: true, - ); - } - - await tester.pumpAndSettle(const Duration(milliseconds: 1000)); - - expect(find.byType(FindMenuWidget), findsOneWidget); - - return Future.value(editor); -} - -Future _prepareWithTextInputForFind( - WidgetTester tester, { - int lines = 1, - String pattern = "Welcome", -}) async { - const text = 'Welcome to Appflowy 😁'; - const textInputKey = Key('findTextField'); - final editor = tester.editor; - for (var i = 0; i < lines; i++) { - editor.insertTextNode(text); - } - await editor.startTesting(); - await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); - - if (Platform.isWindows || Platform.isLinux) { - await editor.pressLogicKey( - key: LogicalKeyboardKey.keyH, - isControlPressed: true, - ); - } else { - await editor.pressLogicKey( - key: LogicalKeyboardKey.keyH, - isMetaPressed: true, - ); - } - - await tester.pumpAndSettle(const Duration(milliseconds: 1000)); - - expect(find.byType(FindMenuWidget), findsOneWidget); - - await tester.tap(find.byKey(textInputKey)); - await tester.enterText(find.byKey(textInputKey), pattern); - await tester.pumpAndSettle(); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - - //pressing enter should trigger the findAndHighlight method, which - //will find the pattern inside the editor. - await editor.pressLogicKey( - key: LogicalKeyboardKey.enter, - ); - - return Future.value(editor); -} +// import 'dart:io'; +// import 'package:flutter/material.dart'; +// import 'package:flutter/services.dart'; +// import 'package:flutter_test/flutter_test.dart'; + +// import 'package:appflowy_editor/src/editor/find_replace_menu/find_replace_widget.dart'; +// import 'package:appflowy_editor/appflowy_editor.dart'; +// import '../../infra/test_editor.dart'; + +// void main() async { +// setUpAll(() { +// TestWidgetsFlutterBinding.ensureInitialized(); +// }); + +// group('find_replace_menu.dart replaceMenu', () { +// testWidgets('replace menu appears properly', (tester) async { +// await _prepare(tester, lines: 3); + +// //the prepare method only checks if FindMenuWidget is present +// //so here we also check if FindMenuWidget contains TextField +// //and IconButtons or not. +// //and whether there are two textfields for replace menu as well. +// expect(find.byType(TextField), findsNWidgets(2)); +// expect(find.byType(IconButton), findsAtLeastNWidgets(6)); +// }); + +// testWidgets('replace menu disappears when close is called', (tester) async { +// await _prepare(tester, lines: 3); + +// await tester.tap(find.byKey(const Key('closeButton'))); +// await tester.pumpAndSettle(); + +// expect(find.byType(FindMenuWidget), findsNothing); +// expect(find.byType(TextField), findsNothing); +// expect(find.byType(IconButton), findsNothing); +// }); + +// testWidgets('replace menu does not work when find is not called', +// (tester) async { +// const textInputKey = Key('replaceTextField'); +// const pattern = 'Flutter'; +// final editor = await _prepare(tester); + +// await tester.tap(find.byKey(textInputKey)); +// await tester.enterText(find.byKey(textInputKey), pattern); +// await tester.pumpAndSettle(); +// await tester.testTextInput.receiveAction(TextInputAction.done); +// await tester.pumpAndSettle(); + +// //pressing enter should trigger the replaceSelected method +// await editor.pressLogicKey( +// key: LogicalKeyboardKey.enter, +// ); +// await tester.pumpAndSettle(); + +// //note our document only has one node +// final textNode = editor.nodeAtPath([0]) as TextNode; +// const expectedText = 'Welcome to Appflowy 😁'; +// expect(textNode.toPlainText(), expectedText); +// }); + +// testWidgets('replace does not change text when no match is found', +// (tester) async { +// const textInputKey = Key('replaceTextField'); +// const pattern = 'Flutter'; + +// final editor = await _prepareWithTextInputForFind( +// tester, +// lines: 1, +// pattern: pattern, +// ); + +// await tester.tap(find.byKey(textInputKey)); +// await tester.enterText(find.byKey(textInputKey), pattern); +// await tester.pumpAndSettle(); +// await tester.testTextInput.receiveAction(TextInputAction.done); +// await tester.pumpAndSettle(); + +// await editor.pressLogicKey( +// key: LogicalKeyboardKey.enter, +// ); +// await tester.pumpAndSettle(); + +// final textNode = editor.nodeAtPath([0]) as TextNode; +// const expectedText = 'Welcome to Appflowy 😁'; +// expect(textNode.toPlainText(), expectedText); +// }); + +// testWidgets('found selected match is replaced properly', (tester) async { +// const patternToBeFound = 'Welcome'; +// const replacePattern = 'Salute'; +// const textInputKey = Key('replaceTextField'); + +// final editor = await _prepareWithTextInputForFind( +// tester, +// lines: 3, +// pattern: patternToBeFound, +// ); + +// //check if matches are not yet replaced +// var textNode = editor.nodeAtPath([2]) as TextNode; +// var expectedText = '$patternToBeFound to Appflowy 😁'; +// expect(textNode.toPlainText(), expectedText); + +// //we select the replace text field and provide replacePattern +// await tester.tap(find.byKey(textInputKey)); +// await tester.enterText(find.byKey(textInputKey), replacePattern); +// await tester.pumpAndSettle(); +// await tester.testTextInput.receiveAction(TextInputAction.done); +// await tester.pumpAndSettle(); + +// await editor.pressLogicKey( +// key: LogicalKeyboardKey.enter, +// ); + +// await tester.pumpAndSettle(); + +// //we know that the findAndHighlight method selects the last +// //matched occurance in the editor document. +// textNode = editor.nodeAtPath([2]) as TextNode; +// expectedText = '$replacePattern to Appflowy 😁'; +// expect(textNode.toPlainText(), expectedText); + +// //also check if other matches are not yet replaced +// textNode = editor.nodeAtPath([1]) as TextNode; +// expectedText = '$patternToBeFound to Appflowy 😁'; +// expect(textNode.toPlainText(), expectedText); +// }); + +// testWidgets('replace all on found matches', (tester) async { +// const patternToBeFound = 'Welcome'; +// const replacePattern = 'Salute'; +// const expectedText = '$replacePattern to Appflowy 😁'; +// const lines = 3; + +// const textInputKey = Key('replaceTextField'); +// const replaceAllBtn = Key('replaceAllButton'); + +// final editor = await _prepareWithTextInputForFind( +// tester, +// lines: lines, +// pattern: patternToBeFound, +// ); + +// //check if matches are not yet replaced +// var textNode = editor.nodeAtPath([2]) as TextNode; +// var originalText = '$patternToBeFound to Appflowy 😁'; +// expect(textNode.toPlainText(), originalText); + +// await tester.tap(find.byKey(textInputKey)); +// await tester.enterText(find.byKey(textInputKey), replacePattern); +// await tester.pumpAndSettle(); +// await tester.testTextInput.receiveAction(TextInputAction.done); +// await tester.pumpAndSettle(); + +// await tester.tap(find.byKey(replaceAllBtn)); +// await tester.pumpAndSettle(); + +// //all matches should be replaced +// for (var i = 0; i < lines; i++) { +// textNode = editor.nodeAtPath([i]) as TextNode; +// expect(textNode.toPlainText(), expectedText); +// } +// }); +// }); +// } + +// Future _prepare( +// WidgetTester tester, { +// int lines = 1, +// }) async { +// const text = 'Welcome to Appflowy 😁'; +// final editor = tester.editor; +// for (var i = 0; i < lines; i++) { +// editor.insertTextNode(text); +// } +// await editor.startTesting(); +// await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + +// if (Platform.isWindows || Platform.isLinux) { +// await editor.pressLogicKey( +// key: LogicalKeyboardKey.keyH, +// isControlPressed: true, +// ); +// } else { +// await editor.pressLogicKey( +// key: LogicalKeyboardKey.keyH, +// isMetaPressed: true, +// ); +// } + +// await tester.pumpAndSettle(const Duration(milliseconds: 1000)); + +// expect(find.byType(FindMenuWidget), findsOneWidget); + +// return Future.value(editor); +// } + +// Future _prepareWithTextInputForFind( +// WidgetTester tester, { +// int lines = 1, +// String pattern = "Welcome", +// }) async { +// const text = 'Welcome to Appflowy 😁'; +// const textInputKey = Key('findTextField'); +// final editor = tester.editor; +// for (var i = 0; i < lines; i++) { +// editor.insertTextNode(text); +// } +// await editor.startTesting(); +// await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + +// if (Platform.isWindows || Platform.isLinux) { +// await editor.pressLogicKey( +// key: LogicalKeyboardKey.keyH, +// isControlPressed: true, +// ); +// } else { +// await editor.pressLogicKey( +// key: LogicalKeyboardKey.keyH, +// isMetaPressed: true, +// ); +// } + +// await tester.pumpAndSettle(const Duration(milliseconds: 1000)); + +// expect(find.byType(FindMenuWidget), findsOneWidget); + +// await tester.tap(find.byKey(textInputKey)); +// await tester.enterText(find.byKey(textInputKey), pattern); +// await tester.pumpAndSettle(); +// await tester.testTextInput.receiveAction(TextInputAction.done); +// await tester.pumpAndSettle(); + +// //pressing enter should trigger the findAndHighlight method, which +// //will find the pattern inside the editor. +// await editor.pressLogicKey( +// key: LogicalKeyboardKey.enter, +// ); + +// return Future.value(editor); +// } diff --git a/test/render/find_replace_menu/search_algorithm_test.dart b/test/render/find_replace_menu/search_algorithm_test.dart index 5b094ae34..02170847e 100644 --- a/test/render/find_replace_menu/search_algorithm_test.dart +++ b/test/render/find_replace_menu/search_algorithm_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:appflowy_editor/src/render/find_replace_menu/search_algorithm.dart'; +import 'package:appflowy_editor/src/editor/find_replace_menu/search_algorithm.dart'; void main() { group('search_algorithm_test.dart', () { From c54250aeae142529b2f63719bcca0749a98409ad Mon Sep 17 00:00:00 2001 From: MayurSMahajan Date: Mon, 17 Jul 2023 20:13:22 +0530 Subject: [PATCH 23/45] refactor: add shortcut for find --- .../standard_block_components.dart | 3 + .../command_shortcut_events.dart | 1 + .../find_replace_command.dart | 60 +++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart diff --git a/lib/src/editor/block_component/standard_block_components.dart b/lib/src/editor/block_component/standard_block_components.dart index 70634a76b..1bc5415c2 100644 --- a/lib/src/editor/block_component/standard_block_components.dart +++ b/lib/src/editor/block_component/standard_block_components.dart @@ -117,6 +117,9 @@ final List standardCommandShortcutEvents = [ indentCommand, outdentCommand, + // + openFindDialog, + exitEditingCommand, // diff --git a/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/command_shortcut_events.dart b/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/command_shortcut_events.dart index da389fd95..3fd1edf37 100644 --- a/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/command_shortcut_events.dart +++ b/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/command_shortcut_events.dart @@ -16,3 +16,4 @@ export 'copy_command.dart'; export 'paste_command.dart'; export 'delete_command.dart'; export 'remove_word_command.dart'; +export 'find_replace_command.dart'; diff --git a/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart b/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart new file mode 100644 index 000000000..bbddb198f --- /dev/null +++ b/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart @@ -0,0 +1,60 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/editor/find_replace_menu/find_menu_service.dart'; +import 'package:flutter/material.dart'; + +/// Show the slash menu +/// +/// - support +/// - desktop +/// - web +/// +final CommandShortcutEvent openFindDialog = CommandShortcutEvent( + key: 'show the find dialog', + command: 'ctrl+f', + macOSCommand: 'cmd+f', + handler: (editorState) => _showFindDialog( + editorState, + ), +); + +FindReplaceService? _findReplaceService; +KeyEventResult _showFindDialog( + EditorState editorState, +) { + if (PlatformExtension.isMobile) { + return KeyEventResult.ignored; + } + + final selection = editorState.selection; + if (selection == null) { + return KeyEventResult.ignored; + } + + // // delete the selection + // if (!selection.isCollapsed) { + // await editorState.deleteSelection(selection); + // } + + final afterSelection = editorState.selection; + if (afterSelection == null || !afterSelection.isCollapsed) { + assert(false, 'the selection should be collapsed'); + return KeyEventResult.handled; + } + + // show the slash menu + () { + // this code is copied from the the old editor. + // TODO: refactor this code + final context = editorState.getNodeAtPath(selection.start.path)?.context; + if (context != null) { + _findReplaceService = FindReplaceMenu( + context: context, + editorState: editorState, + replaceFlag: false, + ); + _findReplaceService?.show(); + } + }(); + + return KeyEventResult.handled; +} From a9c7617a2c8b31007c01df3c7aaa21e50da5800f Mon Sep 17 00:00:00 2001 From: MayurSMahajan Date: Tue, 18 Jul 2023 19:43:27 +0530 Subject: [PATCH 24/45] refactor: use new api --- .../find_replace_menu/find_replace_widget.dart | 6 +++--- .../find_replace_menu/search_service.dart | 17 +++++++---------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/lib/src/editor/find_replace_menu/find_replace_widget.dart b/lib/src/editor/find_replace_menu/find_replace_widget.dart index 4cc8758c7..0f748ac94 100644 --- a/lib/src/editor/find_replace_menu/find_replace_widget.dart +++ b/lib/src/editor/find_replace_menu/find_replace_widget.dart @@ -41,9 +41,9 @@ class _FindMenuWidgetState extends State { Row( children: [ IconButton( - onPressed: () => setState(() { - replaceFlag = !replaceFlag; - }), + onPressed: () => setState( + () => replaceFlag = !replaceFlag, + ), icon: replaceFlag ? const Icon(Icons.expand_less) : const Icon(Icons.expand_more), diff --git a/lib/src/editor/find_replace_menu/search_service.dart b/lib/src/editor/find_replace_menu/search_service.dart index 7b17dcd46..aa4e4be9b 100644 --- a/lib/src/editor/find_replace_menu/search_service.dart +++ b/lib/src/editor/find_replace_menu/search_service.dart @@ -31,11 +31,11 @@ class SearchService { if (contents.isEmpty || pattern.isEmpty) return; final firstNode = contents.firstWhere( - (element) => element is TextNode, + (el) => el.delta != null, ); final lastNode = contents.lastWhere( - (element) => element is TextNode, + (el) => el.delta != null, ); //iterate within all the text nodes of the document. @@ -47,11 +47,11 @@ class SearchService { //traversing all the nodes for (final n in nodes) { - if (n is TextNode) { + if (n.delta != null) { //matches list will contain the offsets where the desired word, //is found. List matches = - searchAlgorithm.boyerMooreSearch(pattern, n.toPlainText()); + searchAlgorithm.boyerMooreSearch(pattern, n.delta!.toPlainText()); //we will store this list of offsets along with their path, //in a list of positions. for (int matchedOffset in matches) { @@ -74,16 +74,14 @@ class SearchService { selectedIndex = selectedIndex - 1 < 0 ? matchedPositions.length - 1 : --selectedIndex; - final match = matchedPositions[selectedIndex]; + Position match = matchedPositions[selectedIndex]; _selectWordAtPosition(match); - //FIXME: selecting a word should scroll editor automatically. } else { selectedIndex = (selectedIndex + 1) < matchedPositions.length ? ++selectedIndex : 0; final match = matchedPositions[selectedIndex]; _selectWordAtPosition(match); - //FIXME: selecting a word should scroll editor automatically. } } @@ -107,9 +105,8 @@ class SearchService { ); editorState.undoManager.forgetRecentUndo(); - final textNode = editorState.service.selectionService.currentSelectedNodes - .whereType() - .first; + final textNodes = editorState.service.selectionService.currentSelectedNodes; + final textNode = textNodes.firstWhere((t) => t.delta != null); final transaction = editorState.transaction; From 5aa776ff71e74453c89ba196da6116a75f22095a Mon Sep 17 00:00:00 2001 From: MayurSMahajan Date: Wed, 19 Jul 2023 15:28:37 +0530 Subject: [PATCH 25/45] refactor: add abstrac class search algo --- .../find_replace_menu/search_algorithm.dart | 13 +- .../find_replace_menu/search_service.dart | 4 +- .../search_algorithm_test.dart | 112 +++++++++--------- 3 files changed, 69 insertions(+), 60 deletions(-) diff --git a/lib/src/editor/find_replace_menu/search_algorithm.dart b/lib/src/editor/find_replace_menu/search_algorithm.dart index 7ec0c766e..048e7fab2 100644 --- a/lib/src/editor/find_replace_menu/search_algorithm.dart +++ b/lib/src/editor/find_replace_menu/search_algorithm.dart @@ -1,10 +1,19 @@ import 'dart:math' as math; -class SearchAlgorithm { +/// If someone wants to use their own implementation for the search algorithm +/// They can do so by extending this abstract class and overriding its +/// `searchMethod(String pattern, String text)`, here `pattern` is the sequence of +/// characters that are to be searched within the `text`. +abstract class SearchAlgorithm { + List searchMethod(String pattern, String text); +} + +class BoyerMoore extends SearchAlgorithm { //This is a standard algorithm used for searching patterns in long text samples //It is more efficient than brute force searching because it is able to skip //characters that will never possibly match with required pattern. - List boyerMooreSearch(String pattern, String text) { + @override + List searchMethod(String pattern, String text) { int m = pattern.length; int n = text.length; diff --git a/lib/src/editor/find_replace_menu/search_service.dart b/lib/src/editor/find_replace_menu/search_service.dart index aa4e4be9b..1efd87b78 100644 --- a/lib/src/editor/find_replace_menu/search_service.dart +++ b/lib/src/editor/find_replace_menu/search_service.dart @@ -11,7 +11,7 @@ class SearchService { //the position here consists of the node and the starting offset of the //matched pattern. We will use this to traverse between the matched patterns. List matchedPositions = []; - SearchAlgorithm searchAlgorithm = SearchAlgorithm(); + SearchAlgorithm searchAlgorithm = BoyerMoore(); String queriedPattern = ''; int selectedIndex = 0; @@ -51,7 +51,7 @@ class SearchService { //matches list will contain the offsets where the desired word, //is found. List matches = - searchAlgorithm.boyerMooreSearch(pattern, n.delta!.toPlainText()); + searchAlgorithm.searchMethod(pattern, n.delta!.toPlainText()); //we will store this list of offsets along with their path, //in a list of positions. for (int matchedOffset in matches) { diff --git a/test/render/find_replace_menu/search_algorithm_test.dart b/test/render/find_replace_menu/search_algorithm_test.dart index 02170847e..1ff4f3e12 100644 --- a/test/render/find_replace_menu/search_algorithm_test.dart +++ b/test/render/find_replace_menu/search_algorithm_test.dart @@ -1,71 +1,71 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:appflowy_editor/src/editor/find_replace_menu/search_algorithm.dart'; +// import 'package:flutter_test/flutter_test.dart'; +// import 'package:appflowy_editor/src/editor/find_replace_menu/search_algorithm.dart'; -void main() { - group('search_algorithm_test.dart', () { - late SearchAlgorithm searchAlgorithm; +// void main() { +// group('search_algorithm_test.dart', () { +// late SearchAlgorithm searchAlgorithm; - setUp(() { - searchAlgorithm = SearchAlgorithm(); - }); +// setUp(() { +// searchAlgorithm = SearchAlgorithm(); +// }); - test('search algorithm returns the index of the only found pattern', () { - const pattern = 'Appflowy'; - const text = 'Welcome to Appflowy 😁'; +// test('search algorithm returns the index of the only found pattern', () { +// const pattern = 'Appflowy'; +// const text = 'Welcome to Appflowy 😁'; - List result = searchAlgorithm.boyerMooreSearch(pattern, text); - expect(result, [11]); - }); +// List result = searchAlgorithm.boyerMooreSearch(pattern, text); +// expect(result, [11]); +// }); - test('search algorithm returns the index of the multiple found patterns', - () { - const pattern = 'Appflowy'; - const text = ''' -Welcome to Appflowy 😁. Appflowy is an open-source alternative to Notion. -With Appflowy, you can build detailed lists of to-do for different -projects while tracking the status of each one. With Appflowy, you can -visualize items in a database moving through stages of a process, or -grouped by property. Design and modify Appflowy your way with an -open core codebase. Appflowy is built with Flutter and Rust. - '''; +// test('search algorithm returns the index of the multiple found patterns', +// () { +// const pattern = 'Appflowy'; +// const text = ''' +// Welcome to Appflowy 😁. Appflowy is an open-source alternative to Notion. +// With Appflowy, you can build detailed lists of to-do for different +// projects while tracking the status of each one. With Appflowy, you can +// visualize items in a database moving through stages of a process, or +// grouped by property. Design and modify Appflowy your way with an +// open core codebase. Appflowy is built with Flutter and Rust. +// '''; - List result = searchAlgorithm.boyerMooreSearch(pattern, text); - expect(result, [11, 24, 80, 196, 324, 371]); - }); +// List result = searchAlgorithm.boyerMooreSearch(pattern, text); +// expect(result, [11, 24, 80, 196, 324, 371]); +// }); - test('search algorithm returns empty list if pattern is not found', () { - const pattern = 'Flutter'; - const text = 'Welcome to Appflowy 😁'; +// test('search algorithm returns empty list if pattern is not found', () { +// const pattern = 'Flutter'; +// const text = 'Welcome to Appflowy 😁'; - final result = searchAlgorithm.boyerMooreSearch(pattern, text); +// final result = searchAlgorithm.boyerMooreSearch(pattern, text); - expect(result, []); - }); +// expect(result, []); +// }); - test('search algorithm returns pattern index if pattern is non-ASCII', () { - const pattern = '😁'; - const text = 'Welcome to Appflowy 😁'; +// test('search algorithm returns pattern index if pattern is non-ASCII', () { +// const pattern = '😁'; +// const text = 'Welcome to Appflowy 😁'; - List result = searchAlgorithm.boyerMooreSearch(pattern, text); - expect(result, [20]); - }); +// List result = searchAlgorithm.boyerMooreSearch(pattern, text); +// expect(result, [20]); +// }); - test( - 'search algorithm returns pattern index if pattern is not separate word', - () { - const pattern = 'App'; - const text = 'Welcome to Appflowy 😁'; +// test( +// 'search algorithm returns pattern index if pattern is not separate word', +// () { +// const pattern = 'App'; +// const text = 'Welcome to Appflowy 😁'; - List result = searchAlgorithm.boyerMooreSearch(pattern, text); - expect(result, [11]); - }); +// List result = searchAlgorithm.boyerMooreSearch(pattern, text); +// expect(result, [11]); +// }); - test('search algorithm returns empty list bcz it is case sensitive', () { - const pattern = 'APPFLOWY'; - const text = 'Welcome to Appflowy 😁'; +// test('search algorithm returns empty list bcz it is case sensitive', () { +// const pattern = 'APPFLOWY'; +// const text = 'Welcome to Appflowy 😁'; - List result = searchAlgorithm.boyerMooreSearch(pattern, text); - expect(result, []); - }); - }); -} +// List result = searchAlgorithm.boyerMooreSearch(pattern, text); +// expect(result, []); +// }); +// }); +// } From 4a88b420e0a49b4272978b114327e2fcc694baae Mon Sep 17 00:00:00 2001 From: MayurSMahajan Date: Wed, 19 Jul 2023 15:29:13 +0530 Subject: [PATCH 26/45] fix: avoid multiple instances of find dialog --- lib/src/editor/find_replace_menu/find_menu_service.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/src/editor/find_replace_menu/find_menu_service.dart b/lib/src/editor/find_replace_menu/find_menu_service.dart index 06e9fc4e4..cd8b14aca 100644 --- a/lib/src/editor/find_replace_menu/find_menu_service.dart +++ b/lib/src/editor/find_replace_menu/find_menu_service.dart @@ -8,6 +8,8 @@ abstract class FindReplaceService { void dismiss(); } +OverlayEntry? _findReplaceMenuEntry; + class FindReplaceMenu implements FindReplaceService { FindReplaceMenu({ required this.context, @@ -21,7 +23,6 @@ class FindReplaceMenu implements FindReplaceService { final double topOffset = 52; final double rightOffset = 40; - OverlayEntry? _findReplaceMenuEntry; bool _selectionUpdateByInner = false; @override @@ -44,7 +45,9 @@ class FindReplaceMenu implements FindReplaceService { @override void show() { - dismiss(); + if (_findReplaceMenuEntry != null) { + dismiss(); + } final selectionService = editorState.service.selectionService; final selectionRects = selectionService.selectionRects; From 029a6c49d4d7bf6b0dd20e1171d7f56fabdba92f Mon Sep 17 00:00:00 2001 From: MayurSMahajan Date: Wed, 19 Jul 2023 16:05:53 +0530 Subject: [PATCH 27/45] refactor: xazin's suggestions --- .../find_replace_widget.dart | 59 ++++++++++--------- lib/src/history/undo_manager.dart | 5 +- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/lib/src/editor/find_replace_menu/find_replace_widget.dart b/lib/src/editor/find_replace_menu/find_replace_widget.dart index 0f748ac94..49d23d7bf 100644 --- a/lib/src/editor/find_replace_menu/find_replace_widget.dart +++ b/lib/src/editor/find_replace_menu/find_replace_widget.dart @@ -2,6 +2,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/editor/find_replace_menu/search_service.dart'; import 'package:flutter/material.dart'; +const double _iconSize = 20; + class FindMenuWidget extends StatefulWidget { const FindMenuWidget({ super.key, @@ -48,37 +50,38 @@ class _FindMenuWidgetState extends State { ? const Icon(Icons.expand_less) : const Icon(Icons.expand_more), ), - Padding( - padding: const EdgeInsets.all(6.0), - child: SizedBox( - width: 200, - height: 50, - child: TextField( - key: const Key('findTextField'), - autofocus: true, - controller: findController, - onSubmitted: (_) => _searchPattern(), - decoration: const InputDecoration( - border: OutlineInputBorder(), - hintText: 'Enter text to search', - ), + SizedBox( + width: 200, + height: 30, + child: TextField( + key: const Key('findTextField'), + autofocus: true, + controller: findController, + onSubmitted: (_) => _searchPattern(), + decoration: const InputDecoration( + contentPadding: EdgeInsets.symmetric(horizontal: 8), + border: OutlineInputBorder(), + hintText: 'Find', ), ), ), IconButton( key: const Key('previousMatchButton'), + iconSize: _iconSize, onPressed: () => searchService.navigateToMatch(moveUp: true), icon: const Icon(Icons.arrow_upward), tooltip: 'Previous Match', ), IconButton( key: const Key('nextMatchButton'), + iconSize: _iconSize, onPressed: () => searchService.navigateToMatch(), icon: const Icon(Icons.arrow_downward), tooltip: 'Next Match', ), IconButton( key: const Key('closeButton'), + iconSize: _iconSize, onPressed: () { widget.dismiss(); searchService.findAndHighlight(queriedPattern); @@ -92,32 +95,32 @@ class _FindMenuWidgetState extends State { replaceFlag ? Row( children: [ - Padding( - padding: const EdgeInsets.all(6.0), - child: SizedBox( - width: 200, - height: 50, - child: TextField( - key: const Key('replaceTextField'), - autofocus: false, - controller: replaceController, - onSubmitted: (_) => _replaceSelectedWord(), - decoration: const InputDecoration( - border: OutlineInputBorder(), - hintText: 'Replace', - ), + SizedBox( + width: 200, + height: 30, + child: TextField( + key: const Key('replaceTextField'), + autofocus: false, + controller: replaceController, + onSubmitted: (_) => _replaceSelectedWord(), + decoration: const InputDecoration( + contentPadding: EdgeInsets.symmetric(horizontal: 8), + border: OutlineInputBorder(), + hintText: 'Replace', ), ), ), IconButton( onPressed: () => _replaceSelectedWord(), icon: const Icon(Icons.find_replace), + iconSize: _iconSize, tooltip: 'Replace', ), IconButton( key: const Key('replaceAllButton'), onPressed: () => _replaceAllMatches(), icon: const Icon(Icons.change_circle_outlined), + iconSize: _iconSize, tooltip: 'Replace All', ), ], diff --git a/lib/src/history/undo_manager.dart b/lib/src/history/undo_manager.dart index c0cbb175d..8a3ed45d2 100644 --- a/lib/src/history/undo_manager.dart +++ b/lib/src/history/undo_manager.dart @@ -152,9 +152,8 @@ class UndoManager { void forgetRecentUndo() { Log.editor.debug('forgetRecentUndo'); - if (state == null) { - return; + if (state != null) { + undoStack.pop(); } - undoStack.pop(); } } From 81ca9a37b7e974a71dc08ba94114cb38f602879d Mon Sep 17 00:00:00 2001 From: MayurSMahajan Date: Wed, 19 Jul 2023 17:09:41 +0530 Subject: [PATCH 28/45] chore: separately build input decor --- .../find_replace_widget.dart | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/src/editor/find_replace_menu/find_replace_widget.dart b/lib/src/editor/find_replace_menu/find_replace_widget.dart index 49d23d7bf..82687ad34 100644 --- a/lib/src/editor/find_replace_menu/find_replace_widget.dart +++ b/lib/src/editor/find_replace_menu/find_replace_widget.dart @@ -58,11 +58,7 @@ class _FindMenuWidgetState extends State { autofocus: true, controller: findController, onSubmitted: (_) => _searchPattern(), - decoration: const InputDecoration( - contentPadding: EdgeInsets.symmetric(horizontal: 8), - border: OutlineInputBorder(), - hintText: 'Find', - ), + decoration: _buildInputDecoration("Find"), ), ), IconButton( @@ -103,11 +99,7 @@ class _FindMenuWidgetState extends State { autofocus: false, controller: replaceController, onSubmitted: (_) => _replaceSelectedWord(), - decoration: const InputDecoration( - contentPadding: EdgeInsets.symmetric(horizontal: 8), - border: OutlineInputBorder(), - hintText: 'Replace', - ), + decoration: _buildInputDecoration("Replace"), ), ), IconButton( @@ -148,4 +140,12 @@ class _FindMenuWidgetState extends State { } searchService.replaceAllMatches(replaceController.text); } + + InputDecoration _buildInputDecoration(String hintText) { + return InputDecoration( + contentPadding: const EdgeInsets.symmetric(horizontal: 8), + border: const OutlineInputBorder(), + hintText: hintText, + ); + } } From a89f7a9969aff4dbb8d29208f09e7bfe4a2a664d Mon Sep 17 00:00:00 2001 From: MayurSMahajan Date: Wed, 19 Jul 2023 20:14:36 +0530 Subject: [PATCH 29/45] test: search algorithm --- .../search_algorithm_test.dart | 112 +++++++++--------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/test/render/find_replace_menu/search_algorithm_test.dart b/test/render/find_replace_menu/search_algorithm_test.dart index 1ff4f3e12..d2f079038 100644 --- a/test/render/find_replace_menu/search_algorithm_test.dart +++ b/test/render/find_replace_menu/search_algorithm_test.dart @@ -1,71 +1,71 @@ -// import 'package:flutter_test/flutter_test.dart'; -// import 'package:appflowy_editor/src/editor/find_replace_menu/search_algorithm.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:appflowy_editor/src/editor/find_replace_menu/search_algorithm.dart'; -// void main() { -// group('search_algorithm_test.dart', () { -// late SearchAlgorithm searchAlgorithm; +void main() { + group('search_algorithm_test.dart', () { + late SearchAlgorithm searchAlgorithm; -// setUp(() { -// searchAlgorithm = SearchAlgorithm(); -// }); + setUp(() { + searchAlgorithm = BoyerMoore(); + }); -// test('search algorithm returns the index of the only found pattern', () { -// const pattern = 'Appflowy'; -// const text = 'Welcome to Appflowy 😁'; + test('search algorithm returns the index of the only found pattern', () { + const pattern = 'Appflowy'; + const text = 'Welcome to Appflowy 😁'; -// List result = searchAlgorithm.boyerMooreSearch(pattern, text); -// expect(result, [11]); -// }); + List result = searchAlgorithm.searchMethod(pattern, text); + expect(result, [11]); + }); -// test('search algorithm returns the index of the multiple found patterns', -// () { -// const pattern = 'Appflowy'; -// const text = ''' -// Welcome to Appflowy 😁. Appflowy is an open-source alternative to Notion. -// With Appflowy, you can build detailed lists of to-do for different -// projects while tracking the status of each one. With Appflowy, you can -// visualize items in a database moving through stages of a process, or -// grouped by property. Design and modify Appflowy your way with an -// open core codebase. Appflowy is built with Flutter and Rust. -// '''; + test('search algorithm returns the index of the multiple found patterns', + () { + const pattern = 'Appflowy'; + const text = ''' +Welcome to Appflowy 😁. Appflowy is an open-source alternative to Notion. +With Appflowy, you can build detailed lists of to-do for different +projects while tracking the status of each one. With Appflowy, you can +visualize items in a database moving through stages of a process, or +grouped by property. Design and modify Appflowy your way with an +open core codebase. Appflowy is built with Flutter and Rust. + '''; -// List result = searchAlgorithm.boyerMooreSearch(pattern, text); -// expect(result, [11, 24, 80, 196, 324, 371]); -// }); + List result = searchAlgorithm.searchMethod(pattern, text); + expect(result, [11, 24, 80, 196, 324, 371]); + }); -// test('search algorithm returns empty list if pattern is not found', () { -// const pattern = 'Flutter'; -// const text = 'Welcome to Appflowy 😁'; + test('search algorithm returns empty list if pattern is not found', () { + const pattern = 'Flutter'; + const text = 'Welcome to Appflowy 😁'; -// final result = searchAlgorithm.boyerMooreSearch(pattern, text); + final result = searchAlgorithm.searchMethod(pattern, text); -// expect(result, []); -// }); + expect(result, []); + }); -// test('search algorithm returns pattern index if pattern is non-ASCII', () { -// const pattern = '😁'; -// const text = 'Welcome to Appflowy 😁'; + test('search algorithm returns pattern index if pattern is non-ASCII', () { + const pattern = '😁'; + const text = 'Welcome to Appflowy 😁'; -// List result = searchAlgorithm.boyerMooreSearch(pattern, text); -// expect(result, [20]); -// }); + List result = searchAlgorithm.searchMethod(pattern, text); + expect(result, [20]); + }); -// test( -// 'search algorithm returns pattern index if pattern is not separate word', -// () { -// const pattern = 'App'; -// const text = 'Welcome to Appflowy 😁'; + test( + 'search algorithm returns pattern index if pattern is not separate word', + () { + const pattern = 'App'; + const text = 'Welcome to Appflowy 😁'; -// List result = searchAlgorithm.boyerMooreSearch(pattern, text); -// expect(result, [11]); -// }); + List result = searchAlgorithm.searchMethod(pattern, text); + expect(result, [11]); + }); -// test('search algorithm returns empty list bcz it is case sensitive', () { -// const pattern = 'APPFLOWY'; -// const text = 'Welcome to Appflowy 😁'; + test('search algorithm returns empty list bcz it is case sensitive', () { + const pattern = 'APPFLOWY'; + const text = 'Welcome to Appflowy 😁'; -// List result = searchAlgorithm.boyerMooreSearch(pattern, text); -// expect(result, []); -// }); -// }); -// } + List result = searchAlgorithm.searchMethod(pattern, text); + expect(result, []); + }); + }); +} From bb0a235e49420f85c745b9cc712be48c07db47c5 Mon Sep 17 00:00:00 2001 From: MayurSMahajan Date: Wed, 19 Jul 2023 20:33:13 +0530 Subject: [PATCH 30/45] chore: unhighlight properly --- .../find_replace_widget.dart | 5 +- .../find_replace_menu/search_service.dart | 101 +++++++++++------- 2 files changed, 67 insertions(+), 39 deletions(-) diff --git a/lib/src/editor/find_replace_menu/find_replace_widget.dart b/lib/src/editor/find_replace_menu/find_replace_widget.dart index 82687ad34..67b790f9f 100644 --- a/lib/src/editor/find_replace_menu/find_replace_widget.dart +++ b/lib/src/editor/find_replace_menu/find_replace_widget.dart @@ -80,7 +80,10 @@ class _FindMenuWidgetState extends State { iconSize: _iconSize, onPressed: () { widget.dismiss(); - searchService.findAndHighlight(queriedPattern); + searchService.findAndHighlight( + queriedPattern, + unhighlight: true, + ); setState(() => queriedPattern = ''); }, icon: const Icon(Icons.close), diff --git a/lib/src/editor/find_replace_menu/search_service.dart b/lib/src/editor/find_replace_menu/search_service.dart index 1efd87b78..988af0eeb 100644 --- a/lib/src/editor/find_replace_menu/search_service.dart +++ b/lib/src/editor/find_replace_menu/search_service.dart @@ -18,48 +18,34 @@ class SearchService { /// Finds the pattern in editorState.document and stores it in matchedPositions. /// Calls the highlightMatch method to highlight the pattern /// if it is found. - void findAndHighlight(String pattern) { + void findAndHighlight(String pattern, {bool unhighlight = false}) { if (queriedPattern != pattern) { //this means we have a new pattern, but before we highlight the new matches, //lets unhiglight the old pattern - findAndHighlight(queriedPattern); + findAndHighlight(queriedPattern, unhighlight: true); queriedPattern = pattern; } - final contents = editorState.document.root.children; - - if (contents.isEmpty || pattern.isEmpty) return; - - final firstNode = contents.firstWhere( - (el) => el.delta != null, - ); - - final lastNode = contents.lastWhere( - (el) => el.delta != null, - ); - - //iterate within all the text nodes of the document. - final nodes = NodeIterator( - document: editorState.document, - startNode: firstNode, - endNode: lastNode, - ).toList(); + if (pattern.isEmpty) return; //traversing all the nodes - for (final n in nodes) { - if (n.delta != null) { - //matches list will contain the offsets where the desired word, - //is found. - List matches = - searchAlgorithm.searchMethod(pattern, n.delta!.toPlainText()); - //we will store this list of offsets along with their path, - //in a list of positions. - for (int matchedOffset in matches) { - matchedPositions.add(Position(path: n.path, offset: matchedOffset)); - } - //finally we will highlight all the mathces. - _highlightMatches(n.path, matches, pattern.length); + for (final n in _getAllTextNodes()) { + //matches list will contain the offsets where the desired word, + //is found. + List matches = + searchAlgorithm.searchMethod(pattern, n.delta!.toPlainText()); + //we will store this list of offsets along with their path, + //in a list of positions. + for (int matchedOffset in matches) { + matchedPositions.add(Position(path: n.path, offset: matchedOffset)); } + //finally we will highlight all the mathces. + _highlightMatches( + n.path, + matches, + pattern.length, + unhighlight: unhighlight, + ); } selectedIndex = matchedPositions.length - 1; @@ -144,15 +130,29 @@ class SearchService { /// /// So for example: path= 1, offset= 10, and patternLength= 5 will mean /// that the word is located on path 1 from [1,10] to [1,14] - void _highlightMatches(Path path, List matches, int patternLength) { + void _highlightMatches( + Path path, + List matches, + int patternLength, { + bool unhighlight = false, + }) { for (final match in matches) { Position start = Position(path: path, offset: match); _selectWordAtPosition(start); - formatHighlightColor( - editorState, - '0x6000BCF0', - ); + if (unhighlight) { + final selection = editorState.selection!; + editorState.formatDelta( + selection, + {AppFlowyRichTextKeys.highlightColor: null}, + ); + } else { + formatHighlightColor( + editorState, + '0x6000BCF0', + ); + } + editorState.undoManager.forgetRecentUndo(); } } @@ -165,4 +165,29 @@ class SearchService { editorState.updateSelectionWithReason(Selection(start: start, end: end)); } + + List _getAllTextNodes() { + final contents = editorState.document.root.children; + + if (contents.isEmpty) return []; + + final firstNode = contents.firstWhere( + (el) => el.delta != null, + ); + + final lastNode = contents.lastWhere( + (el) => el.delta != null, + ); + + //iterate within all the text nodes of the document. + final nodes = NodeIterator( + document: editorState.document, + startNode: firstNode, + endNode: lastNode, + ).toList(); + + nodes.removeWhere((node) => node.delta == null); + + return nodes; + } } From a292a05c3886f6db526679e5315cc5b81546b6c5 Mon Sep 17 00:00:00 2001 From: MayurSMahajan Date: Thu, 20 Jul 2023 16:27:46 +0530 Subject: [PATCH 31/45] refactor: replace handler --- .../standard_block_components.dart | 3 ++- .../find_replace_command.dart | 26 +++++++++++++++---- .../find_replace_menu/search_service.dart | 11 ++++---- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/lib/src/editor/block_component/standard_block_components.dart b/lib/src/editor/block_component/standard_block_components.dart index 1bc5415c2..3e75d1ddc 100644 --- a/lib/src/editor/block_component/standard_block_components.dart +++ b/lib/src/editor/block_component/standard_block_components.dart @@ -118,8 +118,9 @@ final List standardCommandShortcutEvents = [ outdentCommand, // - openFindDialog, + ...findAndReplaceCommands, + // exitEditingCommand, // diff --git a/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart b/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart index bbddb198f..77768467f 100644 --- a/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart +++ b/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart @@ -2,6 +2,11 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/editor/find_replace_menu/find_menu_service.dart'; import 'package:flutter/material.dart'; +final List findAndReplaceCommands = [ + openFindDialog, + openReplaceDialog, +]; + /// Show the slash menu /// /// - support @@ -12,15 +17,26 @@ final CommandShortcutEvent openFindDialog = CommandShortcutEvent( key: 'show the find dialog', command: 'ctrl+f', macOSCommand: 'cmd+f', - handler: (editorState) => _showFindDialog( + handler: (editorState) => _showFindAndReplaceDialog( + editorState, + ), +); + +final CommandShortcutEvent openReplaceDialog = CommandShortcutEvent( + key: 'show the find and replace dialog', + command: 'ctrl+h', + macOSCommand: 'cmd+h', + handler: (editorState) => _showFindAndReplaceDialog( editorState, + openReplace: true, ), ); FindReplaceService? _findReplaceService; -KeyEventResult _showFindDialog( - EditorState editorState, -) { +KeyEventResult _showFindAndReplaceDialog( + EditorState editorState, { + bool openReplace = false, +}) { if (PlatformExtension.isMobile) { return KeyEventResult.ignored; } @@ -50,7 +66,7 @@ KeyEventResult _showFindDialog( _findReplaceService = FindReplaceMenu( context: context, editorState: editorState, - replaceFlag: false, + replaceFlag: openReplace, ); _findReplaceService?.show(); } diff --git a/lib/src/editor/find_replace_menu/search_service.dart b/lib/src/editor/find_replace_menu/search_service.dart index 988af0eeb..15905458f 100644 --- a/lib/src/editor/find_replace_menu/search_service.dart +++ b/lib/src/editor/find_replace_menu/search_service.dart @@ -85,14 +85,14 @@ class SearchService { _selectWordAtPosition(matchedPosition); //unhighlight the selected word before it is replaced - formatHighlightColor( - editorState, - '0x6000BCF0', + final selection = editorState.selection!; + editorState.formatDelta( + selection, + {AppFlowyRichTextKeys.highlightColor: null}, ); editorState.undoManager.forgetRecentUndo(); - final textNodes = editorState.service.selectionService.currentSelectedNodes; - final textNode = textNodes.firstWhere((t) => t.delta != null); + final textNode = editorState.getNodeAtPath(matchedPosition.path)!; final transaction = editorState.transaction; @@ -152,7 +152,6 @@ class SearchService { '0x6000BCF0', ); } - editorState.undoManager.forgetRecentUndo(); } } From 1d0814f3385a309b2ad8882b8ee1b72c68aea999 Mon Sep 17 00:00:00 2001 From: MayurSMahajan Date: Thu, 20 Jul 2023 16:41:30 +0530 Subject: [PATCH 32/45] refactor: move tests into new --- .../find_replace_menu_find_test.dart | 409 ++++++++++++++++++ .../find_replace_menu_replace_test.dart | 0 .../search_algorithm_test.dart | 0 .../find_replace_menu_find_test.dart | 409 ------------------ 4 files changed, 409 insertions(+), 409 deletions(-) create mode 100644 test/new/find_replace_menu/find_replace_menu_find_test.dart rename test/{render => new}/find_replace_menu/find_replace_menu_replace_test.dart (100%) rename test/{render => new}/find_replace_menu/search_algorithm_test.dart (100%) delete mode 100644 test/render/find_replace_menu/find_replace_menu_find_test.dart diff --git a/test/new/find_replace_menu/find_replace_menu_find_test.dart b/test/new/find_replace_menu/find_replace_menu_find_test.dart new file mode 100644 index 000000000..bf9552c02 --- /dev/null +++ b/test/new/find_replace_menu/find_replace_menu_find_test.dart @@ -0,0 +1,409 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:appflowy_editor/src/editor/find_replace_menu/find_replace_widget.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; + +import '../infra/testable_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('find_replace_menu.dart findMenu', () { + testWidgets('find menu appears properly', (tester) async { + await _prepareFindDialog(tester, lines: 3); + + //the prepareFindDialog method only checks if FindMenuWidget is present + //so here we also check if FindMenuWidget contains TextField + //and IconButtons or not. + expect(find.byType(TextField), findsOneWidget); + expect(find.byType(IconButton), findsAtLeastNWidgets(4)); + }); + + testWidgets('find menu disappears when close is called', (tester) async { + await _prepareFindDialog(tester, lines: 3); + + //lets check if find menu disappears if the close button is tapped. + await tester.tap(find.byKey(const Key('closeButton'))); + await tester.pumpAndSettle(); + + expect(find.byType(FindMenuWidget), findsNothing); + expect(find.byType(TextField), findsNothing); + expect(find.byType(IconButton), findsNothing); + }); + + // testWidgets('find menu does not work with empty input', (tester) async { + // const pattern = ''; + + // //we are passing empty string for pattern + // final editor = await _prepareWithTextInputForFind( + // tester, + // lines: 1, + // pattern: pattern, + // ); + + // //since the method will not select anything as searched pattern is + // //empty, the current selection should be equal to previous selection. + // final selection = + // editor.editorState.service.selectionService.currentSelection.value; + + // expect(selection, Selection.single(path: [0], startOffset: 0)); + + // //we can do this because there is only one text node. + // final textNode = editor.nodeAtPath([0]) as TextNode; + + // //we expect that nothing is highlighted in our current document. + // expect( + // textNode.allSatisfyInSelection( + // selection!, + // BuiltInAttributeKey.backgroundColor, + // (value) => value == '0x00000000', + // ), + // true, + // ); + // }); + + // testWidgets('find menu works properly when match is not found', + // (tester) async { + // const pattern = 'Flutter'; + + // final editor = await _prepareWithTextInputForFind( + // tester, + // lines: 1, + // pattern: pattern, + // ); + + // //fetching the current selection + // final selection = + // editor.editorState.service.selectionService.currentSelection.value; + + // //since no match is found the current selection should not be different + // //from initial selection. + // expect(selection != null, true); + // expect(selection, Selection.single(path: [0], startOffset: 0)); + // }); + + // testWidgets('found matches are highlighted', (tester) async { + // const pattern = 'Welcome'; + + // final editor = await _prepareWithTextInputForFind( + // tester, + // lines: 3, + // pattern: pattern, + // ); + + // //checking if current selection consists an occurance of matched pattern. + // final selection = + // editor.editorState.service.selectionService.currentSelection.value; + + // //we expect the last occurance of the pattern to be found, thus that should + // //be the current selection. + // expect(selection != null, true); + // expect(selection!.start, Position(path: [2], offset: 0)); + // expect(selection.end, Position(path: [2], offset: pattern.length)); + + // //check whether the node with found occurance of patten is highlighted + // final textNode = editor.nodeAtPath([2]) as TextNode; + + // //we expect that the current selected node is highlighted. + // //we can confirm that by saying that the node's backgroung color is not white. + // expect( + // textNode.allSatisfyInSelection( + // selection, + // BuiltInAttributeKey.backgroundColor, + // (value) => value != '0x00000000', + // ), + // true, + // ); + // }); + + // testWidgets('navigating to previous matches works', (tester) async { + // const pattern = 'Welcome'; + // const previousBtnKey = Key('previousMatchButton'); + + // final editor = await _prepareWithTextInputForFind( + // tester, + // lines: 2, + // pattern: pattern, + // ); + + // //checking if current selection consists an occurance of matched pattern. + // var selection = + // editor.editorState.service.selectionService.currentSelection.value; + + // //we expect the last occurance of the pattern to be found, thus that should + // //be the current selection. + // expect(selection != null, true); + // expect(selection!.start, Position(path: [1], offset: 0)); + // expect(selection.end, Position(path: [1], offset: pattern.length)); + + // //now pressing the icon button for previous match should select + // //node at path [0]. + // await tester.tap(find.byKey(previousBtnKey)); + // await tester.pumpAndSettle(); + + // selection = + // editor.editorState.service.selectionService.currentSelection.value; + + // expect(selection != null, true); + // expect(selection!.start, Position(path: [0], offset: 0)); + // expect(selection.end, Position(path: [0], offset: pattern.length)); + + // //now pressing the icon button for previous match should select + // //node at path [1], since there is no node before node at [0]. + // await tester.tap(find.byKey(previousBtnKey)); + // await tester.pumpAndSettle(); + + // selection = + // editor.editorState.service.selectionService.currentSelection.value; + + // expect(selection != null, true); + // expect(selection!.start, Position(path: [1], offset: 0)); + // expect(selection.end, Position(path: [1], offset: pattern.length)); + // }); + + // testWidgets('navigating to next matches works', (tester) async { + // const pattern = 'Welcome'; + // const nextBtnKey = Key('nextMatchButton'); + + // final editor = await _prepareWithTextInputForFind( + // tester, + // lines: 3, + // pattern: pattern, + // ); + + // //the last found occurance should be selected + // var selection = + // editor.editorState.service.selectionService.currentSelection.value; + + // expect(selection != null, true); + // expect(selection!.start, Position(path: [2], offset: 0)); + // expect(selection.end, Position(path: [2], offset: pattern.length)); + + // //now pressing the icon button for next match should select + // //node at path [0], since there are no nodes after node at [2]. + // await tester.tap(find.byKey(nextBtnKey)); + // await tester.pumpAndSettle(); + + // selection = + // editor.editorState.service.selectionService.currentSelection.value; + + // expect(selection != null, true); + // expect(selection!.start, Position(path: [0], offset: 0)); + // expect(selection.end, Position(path: [0], offset: pattern.length)); + + // //now pressing the icon button for previous match should select + // //node at path [1]. + // await tester.tap(find.byKey(nextBtnKey)); + // await tester.pumpAndSettle(); + + // selection = + // editor.editorState.service.selectionService.currentSelection.value; + + // expect(selection != null, true); + // expect(selection!.start, Position(path: [1], offset: 0)); + // expect(selection.end, Position(path: [1], offset: pattern.length)); + // }); + + // testWidgets('found matches are unhighlighted when findMenu closed', + // (tester) async { + // const pattern = 'Welcome'; + // const closeBtnKey = Key('closeButton'); + + // final editor = await _prepareWithTextInputForFind( + // tester, + // lines: 3, + // pattern: pattern, + // ); + + // final selection = + // editor.editorState.service.selectionService.currentSelection.value; + + // final textNode = editor.nodeAtPath([2]) as TextNode; + + // //node is highlighted while menu is active + // expect( + // textNode.allSatisfyInSelection( + // selection!, + // BuiltInAttributeKey.backgroundColor, + // (value) => value != '0x00000000', + // ), + // true, + // ); + + // //presses the close button + // await tester.tap(find.byKey(closeBtnKey)); + // await tester.pumpAndSettle(); + + // //closes the findMenuWidget + // expect(find.byType(FindMenuWidget), findsNothing); + + // //node is unhighlighted after the menu is closed + // expect( + // textNode.allSatisfyInSelection( + // selection, + // BuiltInAttributeKey.backgroundColor, + // (value) => value == '0x00000000', + // ), + // true, + // ); + // }); + + // testWidgets('old matches are unhighlighted when new pattern is searched', + // (tester) async { + // const textInputKey = Key('findTextField'); + + // const textLine1 = 'Welcome to Appflowy 😁'; + // const textLine2 = 'Appflowy is made with Flutter, Rust and ❤️'; + // var pattern = 'Welcome'; + + // final editor = tester.editor + // ..insertTextNode(textLine1) + // ..insertTextNode(textLine2); + + // await editor.startTesting(); + // await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + + // if (Platform.isWindows || Platform.isLinux) { + // await editor.pressLogicKey( + // key: LogicalKeyboardKey.keyF, + // isControlPressed: true, + // ); + // } else { + // await editor.pressLogicKey( + // key: LogicalKeyboardKey.keyF, + // isMetaPressed: true, + // ); + // } + + // await tester.pumpAndSettle(const Duration(milliseconds: 1000)); + + // await tester.tap(find.byKey(textInputKey)); + // await tester.enterText(find.byKey(textInputKey), pattern); + // await tester.pumpAndSettle(); + // await tester.testTextInput.receiveAction(TextInputAction.done); + // await tester.pumpAndSettle(); + + // //finds the pattern + // await editor.pressLogicKey( + // key: LogicalKeyboardKey.enter, + // ); + + // //since node at path [1] does not contain match, we expect it + // //to be not highlighted. + // var selection = Selection.single(path: [1], startOffset: 0); + // var textNode = editor.nodeAtPath([1]) as TextNode; + + // expect( + // textNode.allSatisfyInSelection( + // selection, + // BuiltInAttributeKey.backgroundColor, + // (value) => value == '0x00000000', + // ), + // true, + // ); + + // //now we will change the pattern to Flutter and search it + // pattern = 'Flutter'; + // await tester.tap(find.byKey(textInputKey)); + // await tester.enterText(find.byKey(textInputKey), pattern); + // await tester.pumpAndSettle(); + // await tester.testTextInput.receiveAction(TextInputAction.done); + // await tester.pumpAndSettle(); + + // //finds the pattern Flutter + // await editor.pressLogicKey( + // key: LogicalKeyboardKey.enter, + // ); + + // //now we expect the text node at path 1 to contain highlighted pattern + // expect( + // textNode.allSatisfyInSelection( + // selection, + // BuiltInAttributeKey.backgroundColor, + // (value) => value != '0x00000000', + // ), + // true, + // ); + // }); + }); +} + +Future _prepareFindDialog( + WidgetTester tester, { + int lines = 1, +}) async { + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor; + for (var i = 0; i < lines; i++) { + editor.addParagraph(initialText: text); + } + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + + await _pressFindAndReplaceCommand(editor); + + await tester.pumpAndSettle(const Duration(milliseconds: 1000)); + + expect(find.byType(FindMenuWidget), findsOneWidget); +} + +// Future _prepareWithTextInputForFind( +// WidgetTester tester, { +// int lines = 1, +// String pattern = "Welcome", +// }) async { +// const text = 'Welcome to Appflowy 😁'; +// const textInputKey = Key('findTextField'); +// final editor = tester.editor; +// for (var i = 0; i < lines; i++) { +// editor.insertTextNode(text); +// } +// await editor.startTesting(); +// await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + +// if (Platform.isWindows || Platform.isLinux) { +// await editor.pressLogicKey( +// key: LogicalKeyboardKey.keyF, +// isControlPressed: true, +// ); +// } else { +// await editor.pressLogicKey( +// key: LogicalKeyboardKey.keyF, +// isMetaPressed: true, +// ); +// } + +// await tester.pumpAndSettle(const Duration(milliseconds: 1000)); + +// expect(find.byType(FindMenuWidget), findsOneWidget); + +// await tester.tap(find.byKey(textInputKey)); +// await tester.enterText(find.byKey(textInputKey), pattern); +// await tester.pumpAndSettle(); +// await tester.testTextInput.receiveAction(TextInputAction.done); +// await tester.pumpAndSettle(); + +// //pressing enter should trigger the findAndHighlight method, which +// //will find the pattern inside the editor. +// await editor.pressLogicKey( +// key: LogicalKeyboardKey.enter, +// ); + +// return Future.value(editor); +// } + +Future _pressFindAndReplaceCommand( + TestableEditor editor, { + bool openReplace = false, +}) async { + await editor.pressKey( + key: openReplace ? LogicalKeyboardKey.keyH : LogicalKeyboardKey.keyF, + isMetaPressed: Platform.isMacOS, + isControlPressed: !Platform.isMacOS, + ); +} diff --git a/test/render/find_replace_menu/find_replace_menu_replace_test.dart b/test/new/find_replace_menu/find_replace_menu_replace_test.dart similarity index 100% rename from test/render/find_replace_menu/find_replace_menu_replace_test.dart rename to test/new/find_replace_menu/find_replace_menu_replace_test.dart diff --git a/test/render/find_replace_menu/search_algorithm_test.dart b/test/new/find_replace_menu/search_algorithm_test.dart similarity index 100% rename from test/render/find_replace_menu/search_algorithm_test.dart rename to test/new/find_replace_menu/search_algorithm_test.dart diff --git a/test/render/find_replace_menu/find_replace_menu_find_test.dart b/test/render/find_replace_menu/find_replace_menu_find_test.dart deleted file mode 100644 index d6f6a8fce..000000000 --- a/test/render/find_replace_menu/find_replace_menu_find_test.dart +++ /dev/null @@ -1,409 +0,0 @@ -// import 'dart:io'; -// import 'package:flutter/material.dart'; -// import 'package:flutter/services.dart'; -// import 'package:flutter_test/flutter_test.dart'; - -// import 'package:appflowy_editor/src/editor/find_replace_menu/find_replace_widget.dart'; -// import 'package:appflowy_editor/appflowy_editor.dart'; -// import '../../infra/test_editor.dart'; - -// void main() async { -// setUpAll(() { -// TestWidgetsFlutterBinding.ensureInitialized(); -// }); - -// group('find_replace_menu.dart findMenu', () { -// testWidgets('find menu appears properly', (tester) async { -// await _prepare(tester, lines: 3); - -// //the prepare method only checks if FindMenuWidget is present -// //so here we also check if FindMenuWidget contains TextField -// //and IconButtons or not. -// expect(find.byType(TextField), findsOneWidget); -// expect(find.byType(IconButton), findsAtLeastNWidgets(4)); -// }); - -// testWidgets('find menu disappears when close is called', (tester) async { -// await _prepare(tester, lines: 3); - -// //lets check if find menu disappears if the close button is tapped. -// await tester.tap(find.byKey(const Key('closeButton'))); -// await tester.pumpAndSettle(); - -// expect(find.byType(FindMenuWidget), findsNothing); -// expect(find.byType(TextField), findsNothing); -// expect(find.byType(IconButton), findsNothing); -// }); - -// testWidgets('find menu does not work with empty input', (tester) async { -// const pattern = ''; - -// //we are passing empty string for pattern -// final editor = await _prepareWithTextInputForFind( -// tester, -// lines: 1, -// pattern: pattern, -// ); - -// //since the method will not select anything as searched pattern is -// //empty, the current selection should be equal to previous selection. -// final selection = -// editor.editorState.service.selectionService.currentSelection.value; - -// expect(selection, Selection.single(path: [0], startOffset: 0)); - -// //we can do this because there is only one text node. -// final textNode = editor.nodeAtPath([0]) as TextNode; - -// //we expect that nothing is highlighted in our current document. -// expect( -// textNode.allSatisfyInSelection( -// selection!, -// BuiltInAttributeKey.backgroundColor, -// (value) => value == '0x00000000', -// ), -// true, -// ); -// }); - -// testWidgets('find menu works properly when match is not found', -// (tester) async { -// const pattern = 'Flutter'; - -// final editor = await _prepareWithTextInputForFind( -// tester, -// lines: 1, -// pattern: pattern, -// ); - -// //fetching the current selection -// final selection = -// editor.editorState.service.selectionService.currentSelection.value; - -// //since no match is found the current selection should not be different -// //from initial selection. -// expect(selection != null, true); -// expect(selection, Selection.single(path: [0], startOffset: 0)); -// }); - -// testWidgets('found matches are highlighted', (tester) async { -// const pattern = 'Welcome'; - -// final editor = await _prepareWithTextInputForFind( -// tester, -// lines: 3, -// pattern: pattern, -// ); - -// //checking if current selection consists an occurance of matched pattern. -// final selection = -// editor.editorState.service.selectionService.currentSelection.value; - -// //we expect the last occurance of the pattern to be found, thus that should -// //be the current selection. -// expect(selection != null, true); -// expect(selection!.start, Position(path: [2], offset: 0)); -// expect(selection.end, Position(path: [2], offset: pattern.length)); - -// //check whether the node with found occurance of patten is highlighted -// final textNode = editor.nodeAtPath([2]) as TextNode; - -// //we expect that the current selected node is highlighted. -// //we can confirm that by saying that the node's backgroung color is not white. -// expect( -// textNode.allSatisfyInSelection( -// selection, -// BuiltInAttributeKey.backgroundColor, -// (value) => value != '0x00000000', -// ), -// true, -// ); -// }); - -// testWidgets('navigating to previous matches works', (tester) async { -// const pattern = 'Welcome'; -// const previousBtnKey = Key('previousMatchButton'); - -// final editor = await _prepareWithTextInputForFind( -// tester, -// lines: 2, -// pattern: pattern, -// ); - -// //checking if current selection consists an occurance of matched pattern. -// var selection = -// editor.editorState.service.selectionService.currentSelection.value; - -// //we expect the last occurance of the pattern to be found, thus that should -// //be the current selection. -// expect(selection != null, true); -// expect(selection!.start, Position(path: [1], offset: 0)); -// expect(selection.end, Position(path: [1], offset: pattern.length)); - -// //now pressing the icon button for previous match should select -// //node at path [0]. -// await tester.tap(find.byKey(previousBtnKey)); -// await tester.pumpAndSettle(); - -// selection = -// editor.editorState.service.selectionService.currentSelection.value; - -// expect(selection != null, true); -// expect(selection!.start, Position(path: [0], offset: 0)); -// expect(selection.end, Position(path: [0], offset: pattern.length)); - -// //now pressing the icon button for previous match should select -// //node at path [1], since there is no node before node at [0]. -// await tester.tap(find.byKey(previousBtnKey)); -// await tester.pumpAndSettle(); - -// selection = -// editor.editorState.service.selectionService.currentSelection.value; - -// expect(selection != null, true); -// expect(selection!.start, Position(path: [1], offset: 0)); -// expect(selection.end, Position(path: [1], offset: pattern.length)); -// }); - -// testWidgets('navigating to next matches works', (tester) async { -// const pattern = 'Welcome'; -// const nextBtnKey = Key('nextMatchButton'); - -// final editor = await _prepareWithTextInputForFind( -// tester, -// lines: 3, -// pattern: pattern, -// ); - -// //the last found occurance should be selected -// var selection = -// editor.editorState.service.selectionService.currentSelection.value; - -// expect(selection != null, true); -// expect(selection!.start, Position(path: [2], offset: 0)); -// expect(selection.end, Position(path: [2], offset: pattern.length)); - -// //now pressing the icon button for next match should select -// //node at path [0], since there are no nodes after node at [2]. -// await tester.tap(find.byKey(nextBtnKey)); -// await tester.pumpAndSettle(); - -// selection = -// editor.editorState.service.selectionService.currentSelection.value; - -// expect(selection != null, true); -// expect(selection!.start, Position(path: [0], offset: 0)); -// expect(selection.end, Position(path: [0], offset: pattern.length)); - -// //now pressing the icon button for previous match should select -// //node at path [1]. -// await tester.tap(find.byKey(nextBtnKey)); -// await tester.pumpAndSettle(); - -// selection = -// editor.editorState.service.selectionService.currentSelection.value; - -// expect(selection != null, true); -// expect(selection!.start, Position(path: [1], offset: 0)); -// expect(selection.end, Position(path: [1], offset: pattern.length)); -// }); - -// testWidgets('found matches are unhighlighted when findMenu closed', -// (tester) async { -// const pattern = 'Welcome'; -// const closeBtnKey = Key('closeButton'); - -// final editor = await _prepareWithTextInputForFind( -// tester, -// lines: 3, -// pattern: pattern, -// ); - -// final selection = -// editor.editorState.service.selectionService.currentSelection.value; - -// final textNode = editor.nodeAtPath([2]) as TextNode; - -// //node is highlighted while menu is active -// expect( -// textNode.allSatisfyInSelection( -// selection!, -// BuiltInAttributeKey.backgroundColor, -// (value) => value != '0x00000000', -// ), -// true, -// ); - -// //presses the close button -// await tester.tap(find.byKey(closeBtnKey)); -// await tester.pumpAndSettle(); - -// //closes the findMenuWidget -// expect(find.byType(FindMenuWidget), findsNothing); - -// //node is unhighlighted after the menu is closed -// expect( -// textNode.allSatisfyInSelection( -// selection, -// BuiltInAttributeKey.backgroundColor, -// (value) => value == '0x00000000', -// ), -// true, -// ); -// }); - -// testWidgets('old matches are unhighlighted when new pattern is searched', -// (tester) async { -// const textInputKey = Key('findTextField'); - -// const textLine1 = 'Welcome to Appflowy 😁'; -// const textLine2 = 'Appflowy is made with Flutter, Rust and ❤️'; -// var pattern = 'Welcome'; - -// final editor = tester.editor -// ..insertTextNode(textLine1) -// ..insertTextNode(textLine2); - -// await editor.startTesting(); -// await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); - -// if (Platform.isWindows || Platform.isLinux) { -// await editor.pressLogicKey( -// key: LogicalKeyboardKey.keyF, -// isControlPressed: true, -// ); -// } else { -// await editor.pressLogicKey( -// key: LogicalKeyboardKey.keyF, -// isMetaPressed: true, -// ); -// } - -// await tester.pumpAndSettle(const Duration(milliseconds: 1000)); - -// await tester.tap(find.byKey(textInputKey)); -// await tester.enterText(find.byKey(textInputKey), pattern); -// await tester.pumpAndSettle(); -// await tester.testTextInput.receiveAction(TextInputAction.done); -// await tester.pumpAndSettle(); - -// //finds the pattern -// await editor.pressLogicKey( -// key: LogicalKeyboardKey.enter, -// ); - -// //since node at path [1] does not contain match, we expect it -// //to be not highlighted. -// var selection = Selection.single(path: [1], startOffset: 0); -// var textNode = editor.nodeAtPath([1]) as TextNode; - -// expect( -// textNode.allSatisfyInSelection( -// selection, -// BuiltInAttributeKey.backgroundColor, -// (value) => value == '0x00000000', -// ), -// true, -// ); - -// //now we will change the pattern to Flutter and search it -// pattern = 'Flutter'; -// await tester.tap(find.byKey(textInputKey)); -// await tester.enterText(find.byKey(textInputKey), pattern); -// await tester.pumpAndSettle(); -// await tester.testTextInput.receiveAction(TextInputAction.done); -// await tester.pumpAndSettle(); - -// //finds the pattern Flutter -// await editor.pressLogicKey( -// key: LogicalKeyboardKey.enter, -// ); - -// //now we expect the text node at path 1 to contain highlighted pattern -// expect( -// textNode.allSatisfyInSelection( -// selection, -// BuiltInAttributeKey.backgroundColor, -// (value) => value != '0x00000000', -// ), -// true, -// ); -// }); -// }); -// } - -// Future _prepare( -// WidgetTester tester, { -// int lines = 1, -// }) async { -// const text = 'Welcome to Appflowy 😁'; -// final editor = tester.editor; -// for (var i = 0; i < lines; i++) { -// editor.insertTextNode(text); -// } -// await editor.startTesting(); -// await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); - -// if (Platform.isWindows || Platform.isLinux) { -// await editor.pressLogicKey( -// key: LogicalKeyboardKey.keyF, -// isControlPressed: true, -// ); -// } else { -// await editor.pressLogicKey( -// key: LogicalKeyboardKey.keyF, -// isMetaPressed: true, -// ); -// } - -// await tester.pumpAndSettle(const Duration(milliseconds: 1000)); - -// expect(find.byType(FindMenuWidget), findsOneWidget); - -// return Future.value(editor); -// } - -// Future _prepareWithTextInputForFind( -// WidgetTester tester, { -// int lines = 1, -// String pattern = "Welcome", -// }) async { -// const text = 'Welcome to Appflowy 😁'; -// const textInputKey = Key('findTextField'); -// final editor = tester.editor; -// for (var i = 0; i < lines; i++) { -// editor.insertTextNode(text); -// } -// await editor.startTesting(); -// await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); - -// if (Platform.isWindows || Platform.isLinux) { -// await editor.pressLogicKey( -// key: LogicalKeyboardKey.keyF, -// isControlPressed: true, -// ); -// } else { -// await editor.pressLogicKey( -// key: LogicalKeyboardKey.keyF, -// isMetaPressed: true, -// ); -// } - -// await tester.pumpAndSettle(const Duration(milliseconds: 1000)); - -// expect(find.byType(FindMenuWidget), findsOneWidget); - -// await tester.tap(find.byKey(textInputKey)); -// await tester.enterText(find.byKey(textInputKey), pattern); -// await tester.pumpAndSettle(); -// await tester.testTextInput.receiveAction(TextInputAction.done); -// await tester.pumpAndSettle(); - -// //pressing enter should trigger the findAndHighlight method, which -// //will find the pattern inside the editor. -// await editor.pressLogicKey( -// key: LogicalKeyboardKey.enter, -// ); - -// return Future.value(editor); -// } From afcf6217c6186eba92a3c632354b93813207c287 Mon Sep 17 00:00:00 2001 From: MayurSMahajan Date: Fri, 21 Jul 2023 20:25:43 +0530 Subject: [PATCH 33/45] test: find menu widget test --- .../find_replace_menu/search_service.dart | 4 +- .../find_replace_menu_find_test.dart | 656 +++++++++--------- 2 files changed, 319 insertions(+), 341 deletions(-) diff --git a/lib/src/editor/find_replace_menu/search_service.dart b/lib/src/editor/find_replace_menu/search_service.dart index 15905458f..e8734e5c6 100644 --- a/lib/src/editor/find_replace_menu/search_service.dart +++ b/lib/src/editor/find_replace_menu/search_service.dart @@ -1,6 +1,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/editor/find_replace_menu/search_algorithm.dart'; +const foundSelectedColor = '0x6000BCF0'; + class SearchService { SearchService({ required this.editorState, @@ -149,7 +151,7 @@ class SearchService { } else { formatHighlightColor( editorState, - '0x6000BCF0', + foundSelectedColor, ); } editorState.undoManager.forgetRecentUndo(); diff --git a/test/new/find_replace_menu/find_replace_menu_find_test.dart b/test/new/find_replace_menu/find_replace_menu_find_test.dart index bf9552c02..0bff0cc05 100644 --- a/test/new/find_replace_menu/find_replace_menu_find_test.dart +++ b/test/new/find_replace_menu/find_replace_menu_find_test.dart @@ -8,13 +8,15 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import '../infra/testable_editor.dart'; +const text = 'Welcome to Appflowy 😁'; + void main() async { setUpAll(() { TestWidgetsFlutterBinding.ensureInitialized(); }); group('find_replace_menu.dart findMenu', () { - testWidgets('find menu appears properly', (tester) async { + testWidgets('appears properly', (tester) async { await _prepareFindDialog(tester, lines: 3); //the prepareFindDialog method only checks if FindMenuWidget is present @@ -22,9 +24,11 @@ void main() async { //and IconButtons or not. expect(find.byType(TextField), findsOneWidget); expect(find.byType(IconButton), findsAtLeastNWidgets(4)); + + await tester.editor.dispose(); }); - testWidgets('find menu disappears when close is called', (tester) async { + testWidgets('disappears when close is called', (tester) async { await _prepareFindDialog(tester, lines: 3); //lets check if find menu disappears if the close button is tapped. @@ -34,302 +38,252 @@ void main() async { expect(find.byType(FindMenuWidget), findsNothing); expect(find.byType(TextField), findsNothing); expect(find.byType(IconButton), findsNothing); + + await tester.editor.dispose(); + }); + + testWidgets('does not highlight anything when empty string searched', + (tester) async { + //we expect nothing to be highlighted + await _prepareFindAndInputPattern(tester, '', true); + }); + + testWidgets('works properly when match is not found', (tester) async { + //we expect nothing to be highlighted + await _prepareFindAndInputPattern(tester, 'Flutter', true); }); - // testWidgets('find menu does not work with empty input', (tester) async { - // const pattern = ''; - - // //we are passing empty string for pattern - // final editor = await _prepareWithTextInputForFind( - // tester, - // lines: 1, - // pattern: pattern, - // ); - - // //since the method will not select anything as searched pattern is - // //empty, the current selection should be equal to previous selection. - // final selection = - // editor.editorState.service.selectionService.currentSelection.value; - - // expect(selection, Selection.single(path: [0], startOffset: 0)); - - // //we can do this because there is only one text node. - // final textNode = editor.nodeAtPath([0]) as TextNode; - - // //we expect that nothing is highlighted in our current document. - // expect( - // textNode.allSatisfyInSelection( - // selection!, - // BuiltInAttributeKey.backgroundColor, - // (value) => value == '0x00000000', - // ), - // true, - // ); - // }); - - // testWidgets('find menu works properly when match is not found', - // (tester) async { - // const pattern = 'Flutter'; - - // final editor = await _prepareWithTextInputForFind( - // tester, - // lines: 1, - // pattern: pattern, - // ); - - // //fetching the current selection - // final selection = - // editor.editorState.service.selectionService.currentSelection.value; - - // //since no match is found the current selection should not be different - // //from initial selection. - // expect(selection != null, true); - // expect(selection, Selection.single(path: [0], startOffset: 0)); - // }); - - // testWidgets('found matches are highlighted', (tester) async { - // const pattern = 'Welcome'; - - // final editor = await _prepareWithTextInputForFind( - // tester, - // lines: 3, - // pattern: pattern, - // ); - - // //checking if current selection consists an occurance of matched pattern. - // final selection = - // editor.editorState.service.selectionService.currentSelection.value; - - // //we expect the last occurance of the pattern to be found, thus that should - // //be the current selection. - // expect(selection != null, true); - // expect(selection!.start, Position(path: [2], offset: 0)); - // expect(selection.end, Position(path: [2], offset: pattern.length)); - - // //check whether the node with found occurance of patten is highlighted - // final textNode = editor.nodeAtPath([2]) as TextNode; - - // //we expect that the current selected node is highlighted. - // //we can confirm that by saying that the node's backgroung color is not white. - // expect( - // textNode.allSatisfyInSelection( - // selection, - // BuiltInAttributeKey.backgroundColor, - // (value) => value != '0x00000000', - // ), - // true, - // ); - // }); - - // testWidgets('navigating to previous matches works', (tester) async { - // const pattern = 'Welcome'; - // const previousBtnKey = Key('previousMatchButton'); - - // final editor = await _prepareWithTextInputForFind( - // tester, - // lines: 2, - // pattern: pattern, - // ); - - // //checking if current selection consists an occurance of matched pattern. - // var selection = - // editor.editorState.service.selectionService.currentSelection.value; - - // //we expect the last occurance of the pattern to be found, thus that should - // //be the current selection. - // expect(selection != null, true); - // expect(selection!.start, Position(path: [1], offset: 0)); - // expect(selection.end, Position(path: [1], offset: pattern.length)); - - // //now pressing the icon button for previous match should select - // //node at path [0]. - // await tester.tap(find.byKey(previousBtnKey)); - // await tester.pumpAndSettle(); - - // selection = - // editor.editorState.service.selectionService.currentSelection.value; - - // expect(selection != null, true); - // expect(selection!.start, Position(path: [0], offset: 0)); - // expect(selection.end, Position(path: [0], offset: pattern.length)); - - // //now pressing the icon button for previous match should select - // //node at path [1], since there is no node before node at [0]. - // await tester.tap(find.byKey(previousBtnKey)); - // await tester.pumpAndSettle(); - - // selection = - // editor.editorState.service.selectionService.currentSelection.value; - - // expect(selection != null, true); - // expect(selection!.start, Position(path: [1], offset: 0)); - // expect(selection.end, Position(path: [1], offset: pattern.length)); - // }); - - // testWidgets('navigating to next matches works', (tester) async { - // const pattern = 'Welcome'; - // const nextBtnKey = Key('nextMatchButton'); - - // final editor = await _prepareWithTextInputForFind( - // tester, - // lines: 3, - // pattern: pattern, - // ); - - // //the last found occurance should be selected - // var selection = - // editor.editorState.service.selectionService.currentSelection.value; - - // expect(selection != null, true); - // expect(selection!.start, Position(path: [2], offset: 0)); - // expect(selection.end, Position(path: [2], offset: pattern.length)); - - // //now pressing the icon button for next match should select - // //node at path [0], since there are no nodes after node at [2]. - // await tester.tap(find.byKey(nextBtnKey)); - // await tester.pumpAndSettle(); - - // selection = - // editor.editorState.service.selectionService.currentSelection.value; - - // expect(selection != null, true); - // expect(selection!.start, Position(path: [0], offset: 0)); - // expect(selection.end, Position(path: [0], offset: pattern.length)); - - // //now pressing the icon button for previous match should select - // //node at path [1]. - // await tester.tap(find.byKey(nextBtnKey)); - // await tester.pumpAndSettle(); - - // selection = - // editor.editorState.service.selectionService.currentSelection.value; - - // expect(selection != null, true); - // expect(selection!.start, Position(path: [1], offset: 0)); - // expect(selection.end, Position(path: [1], offset: pattern.length)); - // }); - - // testWidgets('found matches are unhighlighted when findMenu closed', - // (tester) async { - // const pattern = 'Welcome'; - // const closeBtnKey = Key('closeButton'); - - // final editor = await _prepareWithTextInputForFind( - // tester, - // lines: 3, - // pattern: pattern, - // ); - - // final selection = - // editor.editorState.service.selectionService.currentSelection.value; - - // final textNode = editor.nodeAtPath([2]) as TextNode; - - // //node is highlighted while menu is active - // expect( - // textNode.allSatisfyInSelection( - // selection!, - // BuiltInAttributeKey.backgroundColor, - // (value) => value != '0x00000000', - // ), - // true, - // ); - - // //presses the close button - // await tester.tap(find.byKey(closeBtnKey)); - // await tester.pumpAndSettle(); - - // //closes the findMenuWidget - // expect(find.byType(FindMenuWidget), findsNothing); - - // //node is unhighlighted after the menu is closed - // expect( - // textNode.allSatisfyInSelection( - // selection, - // BuiltInAttributeKey.backgroundColor, - // (value) => value == '0x00000000', - // ), - // true, - // ); - // }); - - // testWidgets('old matches are unhighlighted when new pattern is searched', - // (tester) async { - // const textInputKey = Key('findTextField'); - - // const textLine1 = 'Welcome to Appflowy 😁'; - // const textLine2 = 'Appflowy is made with Flutter, Rust and ❤️'; - // var pattern = 'Welcome'; - - // final editor = tester.editor - // ..insertTextNode(textLine1) - // ..insertTextNode(textLine2); - - // await editor.startTesting(); - // await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); - - // if (Platform.isWindows || Platform.isLinux) { - // await editor.pressLogicKey( - // key: LogicalKeyboardKey.keyF, - // isControlPressed: true, - // ); - // } else { - // await editor.pressLogicKey( - // key: LogicalKeyboardKey.keyF, - // isMetaPressed: true, - // ); - // } - - // await tester.pumpAndSettle(const Duration(milliseconds: 1000)); - - // await tester.tap(find.byKey(textInputKey)); - // await tester.enterText(find.byKey(textInputKey), pattern); - // await tester.pumpAndSettle(); - // await tester.testTextInput.receiveAction(TextInputAction.done); - // await tester.pumpAndSettle(); - - // //finds the pattern - // await editor.pressLogicKey( - // key: LogicalKeyboardKey.enter, - // ); - - // //since node at path [1] does not contain match, we expect it - // //to be not highlighted. - // var selection = Selection.single(path: [1], startOffset: 0); - // var textNode = editor.nodeAtPath([1]) as TextNode; - - // expect( - // textNode.allSatisfyInSelection( - // selection, - // BuiltInAttributeKey.backgroundColor, - // (value) => value == '0x00000000', - // ), - // true, - // ); - - // //now we will change the pattern to Flutter and search it - // pattern = 'Flutter'; - // await tester.tap(find.byKey(textInputKey)); - // await tester.enterText(find.byKey(textInputKey), pattern); - // await tester.pumpAndSettle(); - // await tester.testTextInput.receiveAction(TextInputAction.done); - // await tester.pumpAndSettle(); - - // //finds the pattern Flutter - // await editor.pressLogicKey( - // key: LogicalKeyboardKey.enter, - // ); - - // //now we expect the text node at path 1 to contain highlighted pattern - // expect( - // textNode.allSatisfyInSelection( - // selection, - // BuiltInAttributeKey.backgroundColor, - // (value) => value != '0x00000000', - // ), - // true, - // ); - // }); + testWidgets('highlights properly when match is found', (tester) async { + //we expect something to be highlighted + await _prepareFindAndInputPattern(tester, 'Welcome', false); + }); + + testWidgets('selects found match', (tester) async { + const pattern = 'Welcome'; + + final editor = tester.editor; + editor.addParagraphs(3, initialText: text); + + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + + await _pressFindAndReplaceCommand(editor); + + await tester.pumpAndSettle(); + + expect(find.byType(FindMenuWidget), findsOneWidget); + + await _enterInputIntoFindDialog(tester, pattern); + + await editor.pressKey( + key: LogicalKeyboardKey.enter, + ); + + //checking if current selection consists an occurance of matched pattern. + final selection = + editor.editorState.service.selectionService.currentSelection.value; + + //we expect the last occurance of the pattern to be found and selected, + //thus that should be the current selection. + expect(selection != null, true); + expect(selection!.start, Position(path: [2], offset: 0)); + expect(selection.end, Position(path: [2], offset: pattern.length)); + + await editor.dispose(); + }); + + testWidgets('navigating to previous and next matches works', + (tester) async { + const pattern = 'Welcome'; + const previousBtnKey = Key('previousMatchButton'); + const nextBtnKey = Key('nextMatchButton'); + + final editor = tester.editor; + editor.addParagraphs(2, initialText: text); + + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + + await _pressFindAndReplaceCommand(editor); + + await tester.pumpAndSettle(); + + expect(find.byType(FindMenuWidget), findsOneWidget); + + await _enterInputIntoFindDialog(tester, pattern); + + await editor.pressKey( + key: LogicalKeyboardKey.enter, + ); + + //checking if current selection consists an occurance of matched pattern. + var selection = + editor.editorState.service.selectionService.currentSelection.value; + + //we expect the last occurance of the pattern to be found, thus that should + //be the current selection. + expect(selection != null, true); + expect(selection!.start, Position(path: [1], offset: 0)); + expect(selection.end, Position(path: [1], offset: pattern.length)); + + //now pressing the icon button for previous match should select + //node at path [0]. + await tester.tap(find.byKey(previousBtnKey)); + await tester.pumpAndSettle(); + + selection = + editor.editorState.service.selectionService.currentSelection.value; + + expect(selection != null, true); + expect(selection!.start, Position(path: [0], offset: 0)); + expect(selection.end, Position(path: [0], offset: pattern.length)); + + //now pressing the icon button for previous match should select + //node at path [1], since there is no node before node at [0]. + await tester.tap(find.byKey(previousBtnKey)); + await tester.pumpAndSettle(); + + selection = + editor.editorState.service.selectionService.currentSelection.value; + + expect(selection != null, true); + expect(selection!.start, Position(path: [1], offset: 0)); + expect(selection.end, Position(path: [1], offset: pattern.length)); + + //now pressing the icon button for next match should select + //node at path[0], since there is no node after node at [1]. + await tester.tap(find.byKey(nextBtnKey)); + await tester.pumpAndSettle(); + + selection = + editor.editorState.service.selectionService.currentSelection.value; + + expect(selection != null, true); + expect(selection!.start, Position(path: [0], offset: 0)); + expect(selection.end, Position(path: [0], offset: pattern.length)); + + //now pressing the icon button for next match should select + //node at path [1]. + await tester.tap(find.byKey(nextBtnKey)); + await tester.pumpAndSettle(); + + selection = + editor.editorState.service.selectionService.currentSelection.value; + + expect(selection != null, true); + expect(selection!.start, Position(path: [1], offset: 0)); + expect(selection.end, Position(path: [1], offset: pattern.length)); + + await editor.dispose(); + }); + + testWidgets('found matches are unhighlighted when findMenu closed', + (tester) async { + const pattern = 'Welcome'; + const closeBtnKey = Key('closeButton'); + + final editor = tester.editor; + editor.addParagraphs(3, initialText: text); + + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + + await _pressFindAndReplaceCommand(editor); + + await tester.pumpAndSettle(); + + expect(find.byType(FindMenuWidget), findsOneWidget); + + await _enterInputIntoFindDialog(tester, pattern); + + await editor.pressKey( + key: LogicalKeyboardKey.enter, + ); + + final selection = + editor.editorState.service.selectionService.currentSelection.value; + expect(selection, isNotNull); + + final node = editor.nodeAtPath([2]); + expect(node, isNotNull); + + //node is highlighted while menu is active + _checkIfNotHighlighted(node!, selection!, expectedResult: false); + + //presses the close button + await tester.tap(find.byKey(closeBtnKey)); + await tester.pumpAndSettle(); + + //closes the findMenuWidget + expect(find.byType(FindMenuWidget), findsNothing); + + //we expect that the current selected node is NOT highlighted. + _checkIfNotHighlighted(node, selection, expectedResult: true); + + await editor.dispose(); + }); + + testWidgets('old matches are unhighlighted when new pattern is searched', + (tester) async { + const textLine1 = 'Welcome to Appflowy 😁'; + const textLine2 = 'Appflowy is made with Flutter, Rust and ❤️'; + var pattern = 'Welcome'; + + final editor = tester.editor + ..addParagraph(initialText: textLine1) + ..addParagraph(initialText: textLine2); + + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + + await _pressFindAndReplaceCommand(editor); + + await tester.pumpAndSettle(); + + await _enterInputIntoFindDialog(tester, pattern); + + await editor.pressKey( + key: LogicalKeyboardKey.enter, + ); + + //since node at path [1] does not contain match, we expect it + //to be not highlighted. + final selectionAtNode1 = Selection.single( + path: [1], + startOffset: 0, + endOffset: textLine2.length, + ); + var node = editor.nodeAtPath([1]); + expect(node, isNotNull); + + //we expect that the current node at path 1 to be NOT highlighted. + _checkIfNotHighlighted(node!, selectionAtNode1, expectedResult: true); + + //now we will change the pattern to Flutter and search it + pattern = 'Flutter'; + await _enterInputIntoFindDialog(tester, pattern); + + //finds the pattern Flutter + await editor.pressKey( + key: LogicalKeyboardKey.enter, + ); + + //we expect that the current selected node is highlighted. + _checkIfNotHighlighted(node, selectionAtNode1, expectedResult: false); + + final selectionAtNode0 = Selection.single( + path: [0], + startOffset: 0, + endOffset: textLine1.length, + ); + node = editor.nodeAtPath([0]); + expect(node, isNotNull); + + //we expect that the current node at path 0 to be NOT highlighted. + _checkIfNotHighlighted(node!, selectionAtNode0, expectedResult: true); + + await editor.dispose(); + }); }); } @@ -352,50 +306,42 @@ Future _prepareFindDialog( expect(find.byType(FindMenuWidget), findsOneWidget); } -// Future _prepareWithTextInputForFind( -// WidgetTester tester, { -// int lines = 1, -// String pattern = "Welcome", -// }) async { -// const text = 'Welcome to Appflowy 😁'; -// const textInputKey = Key('findTextField'); -// final editor = tester.editor; -// for (var i = 0; i < lines; i++) { -// editor.insertTextNode(text); -// } -// await editor.startTesting(); -// await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); - -// if (Platform.isWindows || Platform.isLinux) { -// await editor.pressLogicKey( -// key: LogicalKeyboardKey.keyF, -// isControlPressed: true, -// ); -// } else { -// await editor.pressLogicKey( -// key: LogicalKeyboardKey.keyF, -// isMetaPressed: true, -// ); -// } - -// await tester.pumpAndSettle(const Duration(milliseconds: 1000)); - -// expect(find.byType(FindMenuWidget), findsOneWidget); - -// await tester.tap(find.byKey(textInputKey)); -// await tester.enterText(find.byKey(textInputKey), pattern); -// await tester.pumpAndSettle(); -// await tester.testTextInput.receiveAction(TextInputAction.done); -// await tester.pumpAndSettle(); - -// //pressing enter should trigger the findAndHighlight method, which -// //will find the pattern inside the editor. -// await editor.pressLogicKey( -// key: LogicalKeyboardKey.enter, -// ); - -// return Future.value(editor); -// } +Future _prepareFindAndInputPattern( + WidgetTester tester, + String pattern, + bool expectedResult, +) async { + final editor = tester.editor; + editor.addParagraph(initialText: text); + + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + + await _pressFindAndReplaceCommand(editor); + + await tester.pumpAndSettle(); + + expect(find.byType(FindMenuWidget), findsOneWidget); + + await _enterInputIntoFindDialog(tester, pattern); + //pressing enter should trigger the findAndHighlight method, which + //will find the pattern inside the editor. + await editor.pressKey( + key: LogicalKeyboardKey.enter, + ); + + //since the method will not select anything as searched pattern is + //empty, the current selection should be equal to previous selection. + final selection = + Selection.single(path: [0], startOffset: 0, endOffset: text.length); + + final node = editor.nodeAtPath([0]); + expect(node, isNotNull); + + _checkIfNotHighlighted(node!, selection, expectedResult: expectedResult); + + await editor.dispose(); +} Future _pressFindAndReplaceCommand( TestableEditor editor, { @@ -407,3 +353,33 @@ Future _pressFindAndReplaceCommand( isControlPressed: !Platform.isMacOS, ); } + +void _checkIfNotHighlighted( + Node node, + Selection selection, { + bool expectedResult = true, +}) { + //if the expectedResult is true: + //we expect that nothing is highlighted in our current document. + //otherwise: we expect that something is highlighted. + expect( + node.allSatisfyInSelection(selection, (delta) { + return delta.whereType().every( + (el) => el.attributes?[AppFlowyRichTextKeys.highlightColor] == null, + ); + }), + expectedResult, + ); +} + +Future _enterInputIntoFindDialog( + WidgetTester tester, + String pattern, +) async { + const textInputKey = Key('findTextField'); + await tester.tap(find.byKey(textInputKey)); + await tester.enterText(find.byKey(textInputKey), pattern); + await tester.pumpAndSettle(); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); +} From addeb96f7eeae701e803d66cd6ff8a2345283ce9 Mon Sep 17 00:00:00 2001 From: MayurSMahajan Date: Sat, 22 Jul 2023 18:14:11 +0530 Subject: [PATCH 34/45] test: replace menu --- .../find_replace_menu_find_test.dart | 137 +---- .../find_replace_menu_replace_test.dart | 487 +++++++++--------- .../find_replace_menu_utils.dart | 86 ++++ 3 files changed, 356 insertions(+), 354 deletions(-) create mode 100644 test/new/find_replace_menu/find_replace_menu_utils.dart diff --git a/test/new/find_replace_menu/find_replace_menu_find_test.dart b/test/new/find_replace_menu/find_replace_menu_find_test.dart index 0bff0cc05..16104802a 100644 --- a/test/new/find_replace_menu/find_replace_menu_find_test.dart +++ b/test/new/find_replace_menu/find_replace_menu_find_test.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -7,8 +6,7 @@ import 'package:appflowy_editor/src/editor/find_replace_menu/find_replace_widget import 'package:appflowy_editor/appflowy_editor.dart'; import '../infra/testable_editor.dart'; - -const text = 'Welcome to Appflowy 😁'; +import 'find_replace_menu_utils.dart'; void main() async { setUpAll(() { @@ -17,7 +15,7 @@ void main() async { group('find_replace_menu.dart findMenu', () { testWidgets('appears properly', (tester) async { - await _prepareFindDialog(tester, lines: 3); + await prepareFindAndReplaceDialog(tester); //the prepareFindDialog method only checks if FindMenuWidget is present //so here we also check if FindMenuWidget contains TextField @@ -29,7 +27,7 @@ void main() async { }); testWidgets('disappears when close is called', (tester) async { - await _prepareFindDialog(tester, lines: 3); + await prepareFindAndReplaceDialog(tester); //lets check if find menu disappears if the close button is tapped. await tester.tap(find.byKey(const Key('closeButton'))); @@ -67,13 +65,13 @@ void main() async { await editor.startTesting(); await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); - await _pressFindAndReplaceCommand(editor); + await pressFindAndReplaceCommand(editor); await tester.pumpAndSettle(); expect(find.byType(FindMenuWidget), findsOneWidget); - await _enterInputIntoFindDialog(tester, pattern); + await enterInputIntoFindDialog(tester, pattern); await editor.pressKey( key: LogicalKeyboardKey.enter, @@ -104,75 +102,50 @@ void main() async { await editor.startTesting(); await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); - await _pressFindAndReplaceCommand(editor); + await pressFindAndReplaceCommand(editor); await tester.pumpAndSettle(); expect(find.byType(FindMenuWidget), findsOneWidget); - await _enterInputIntoFindDialog(tester, pattern); + await enterInputIntoFindDialog(tester, pattern); await editor.pressKey( key: LogicalKeyboardKey.enter, ); //checking if current selection consists an occurance of matched pattern. - var selection = - editor.editorState.service.selectionService.currentSelection.value; - //we expect the last occurance of the pattern to be found, thus that should //be the current selection. - expect(selection != null, true); - expect(selection!.start, Position(path: [1], offset: 0)); - expect(selection.end, Position(path: [1], offset: pattern.length)); + checkCurrentSelection(editor, [1], 0, pattern.length); //now pressing the icon button for previous match should select //node at path [0]. await tester.tap(find.byKey(previousBtnKey)); await tester.pumpAndSettle(); - selection = - editor.editorState.service.selectionService.currentSelection.value; - - expect(selection != null, true); - expect(selection!.start, Position(path: [0], offset: 0)); - expect(selection.end, Position(path: [0], offset: pattern.length)); + checkCurrentSelection(editor, [0], 0, pattern.length); //now pressing the icon button for previous match should select //node at path [1], since there is no node before node at [0]. await tester.tap(find.byKey(previousBtnKey)); await tester.pumpAndSettle(); - selection = - editor.editorState.service.selectionService.currentSelection.value; - - expect(selection != null, true); - expect(selection!.start, Position(path: [1], offset: 0)); - expect(selection.end, Position(path: [1], offset: pattern.length)); + checkCurrentSelection(editor, [1], 0, pattern.length); //now pressing the icon button for next match should select //node at path[0], since there is no node after node at [1]. await tester.tap(find.byKey(nextBtnKey)); await tester.pumpAndSettle(); - selection = - editor.editorState.service.selectionService.currentSelection.value; - - expect(selection != null, true); - expect(selection!.start, Position(path: [0], offset: 0)); - expect(selection.end, Position(path: [0], offset: pattern.length)); + checkCurrentSelection(editor, [0], 0, pattern.length); //now pressing the icon button for next match should select //node at path [1]. await tester.tap(find.byKey(nextBtnKey)); await tester.pumpAndSettle(); - selection = - editor.editorState.service.selectionService.currentSelection.value; - - expect(selection != null, true); - expect(selection!.start, Position(path: [1], offset: 0)); - expect(selection.end, Position(path: [1], offset: pattern.length)); + checkCurrentSelection(editor, [1], 0, pattern.length); await editor.dispose(); }); @@ -188,13 +161,13 @@ void main() async { await editor.startTesting(); await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); - await _pressFindAndReplaceCommand(editor); + await pressFindAndReplaceCommand(editor); await tester.pumpAndSettle(); expect(find.byType(FindMenuWidget), findsOneWidget); - await _enterInputIntoFindDialog(tester, pattern); + await enterInputIntoFindDialog(tester, pattern); await editor.pressKey( key: LogicalKeyboardKey.enter, @@ -208,7 +181,7 @@ void main() async { expect(node, isNotNull); //node is highlighted while menu is active - _checkIfNotHighlighted(node!, selection!, expectedResult: false); + checkIfNotHighlighted(node!, selection!, expectedResult: false); //presses the close button await tester.tap(find.byKey(closeBtnKey)); @@ -218,7 +191,7 @@ void main() async { expect(find.byType(FindMenuWidget), findsNothing); //we expect that the current selected node is NOT highlighted. - _checkIfNotHighlighted(node, selection, expectedResult: true); + checkIfNotHighlighted(node, selection, expectedResult: true); await editor.dispose(); }); @@ -236,11 +209,11 @@ void main() async { await editor.startTesting(); await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); - await _pressFindAndReplaceCommand(editor); + await pressFindAndReplaceCommand(editor); await tester.pumpAndSettle(); - await _enterInputIntoFindDialog(tester, pattern); + await enterInputIntoFindDialog(tester, pattern); await editor.pressKey( key: LogicalKeyboardKey.enter, @@ -257,11 +230,11 @@ void main() async { expect(node, isNotNull); //we expect that the current node at path 1 to be NOT highlighted. - _checkIfNotHighlighted(node!, selectionAtNode1, expectedResult: true); + checkIfNotHighlighted(node!, selectionAtNode1, expectedResult: true); //now we will change the pattern to Flutter and search it pattern = 'Flutter'; - await _enterInputIntoFindDialog(tester, pattern); + await enterInputIntoFindDialog(tester, pattern); //finds the pattern Flutter await editor.pressKey( @@ -269,7 +242,7 @@ void main() async { ); //we expect that the current selected node is highlighted. - _checkIfNotHighlighted(node, selectionAtNode1, expectedResult: false); + checkIfNotHighlighted(node, selectionAtNode1, expectedResult: false); final selectionAtNode0 = Selection.single( path: [0], @@ -280,32 +253,13 @@ void main() async { expect(node, isNotNull); //we expect that the current node at path 0 to be NOT highlighted. - _checkIfNotHighlighted(node!, selectionAtNode0, expectedResult: true); + checkIfNotHighlighted(node!, selectionAtNode0, expectedResult: true); await editor.dispose(); }); }); } -Future _prepareFindDialog( - WidgetTester tester, { - int lines = 1, -}) async { - const text = 'Welcome to Appflowy 😁'; - final editor = tester.editor; - for (var i = 0; i < lines; i++) { - editor.addParagraph(initialText: text); - } - await editor.startTesting(); - await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); - - await _pressFindAndReplaceCommand(editor); - - await tester.pumpAndSettle(const Duration(milliseconds: 1000)); - - expect(find.byType(FindMenuWidget), findsOneWidget); -} - Future _prepareFindAndInputPattern( WidgetTester tester, String pattern, @@ -317,13 +271,13 @@ Future _prepareFindAndInputPattern( await editor.startTesting(); await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); - await _pressFindAndReplaceCommand(editor); + await pressFindAndReplaceCommand(editor); await tester.pumpAndSettle(); expect(find.byType(FindMenuWidget), findsOneWidget); - await _enterInputIntoFindDialog(tester, pattern); + await enterInputIntoFindDialog(tester, pattern); //pressing enter should trigger the findAndHighlight method, which //will find the pattern inside the editor. await editor.pressKey( @@ -338,48 +292,7 @@ Future _prepareFindAndInputPattern( final node = editor.nodeAtPath([0]); expect(node, isNotNull); - _checkIfNotHighlighted(node!, selection, expectedResult: expectedResult); + checkIfNotHighlighted(node!, selection, expectedResult: expectedResult); await editor.dispose(); } - -Future _pressFindAndReplaceCommand( - TestableEditor editor, { - bool openReplace = false, -}) async { - await editor.pressKey( - key: openReplace ? LogicalKeyboardKey.keyH : LogicalKeyboardKey.keyF, - isMetaPressed: Platform.isMacOS, - isControlPressed: !Platform.isMacOS, - ); -} - -void _checkIfNotHighlighted( - Node node, - Selection selection, { - bool expectedResult = true, -}) { - //if the expectedResult is true: - //we expect that nothing is highlighted in our current document. - //otherwise: we expect that something is highlighted. - expect( - node.allSatisfyInSelection(selection, (delta) { - return delta.whereType().every( - (el) => el.attributes?[AppFlowyRichTextKeys.highlightColor] == null, - ); - }), - expectedResult, - ); -} - -Future _enterInputIntoFindDialog( - WidgetTester tester, - String pattern, -) async { - const textInputKey = Key('findTextField'); - await tester.tap(find.byKey(textInputKey)); - await tester.enterText(find.byKey(textInputKey), pattern); - await tester.pumpAndSettle(); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); -} diff --git a/test/new/find_replace_menu/find_replace_menu_replace_test.dart b/test/new/find_replace_menu/find_replace_menu_replace_test.dart index 57f0e3094..6988e5028 100644 --- a/test/new/find_replace_menu/find_replace_menu_replace_test.dart +++ b/test/new/find_replace_menu/find_replace_menu_replace_test.dart @@ -1,242 +1,245 @@ -// import 'dart:io'; -// import 'package:flutter/material.dart'; -// import 'package:flutter/services.dart'; -// import 'package:flutter_test/flutter_test.dart'; - -// import 'package:appflowy_editor/src/editor/find_replace_menu/find_replace_widget.dart'; -// import 'package:appflowy_editor/appflowy_editor.dart'; -// import '../../infra/test_editor.dart'; - -// void main() async { -// setUpAll(() { -// TestWidgetsFlutterBinding.ensureInitialized(); -// }); - -// group('find_replace_menu.dart replaceMenu', () { -// testWidgets('replace menu appears properly', (tester) async { -// await _prepare(tester, lines: 3); - -// //the prepare method only checks if FindMenuWidget is present -// //so here we also check if FindMenuWidget contains TextField -// //and IconButtons or not. -// //and whether there are two textfields for replace menu as well. -// expect(find.byType(TextField), findsNWidgets(2)); -// expect(find.byType(IconButton), findsAtLeastNWidgets(6)); -// }); - -// testWidgets('replace menu disappears when close is called', (tester) async { -// await _prepare(tester, lines: 3); - -// await tester.tap(find.byKey(const Key('closeButton'))); -// await tester.pumpAndSettle(); - -// expect(find.byType(FindMenuWidget), findsNothing); -// expect(find.byType(TextField), findsNothing); -// expect(find.byType(IconButton), findsNothing); -// }); - -// testWidgets('replace menu does not work when find is not called', -// (tester) async { -// const textInputKey = Key('replaceTextField'); -// const pattern = 'Flutter'; -// final editor = await _prepare(tester); - -// await tester.tap(find.byKey(textInputKey)); -// await tester.enterText(find.byKey(textInputKey), pattern); -// await tester.pumpAndSettle(); -// await tester.testTextInput.receiveAction(TextInputAction.done); -// await tester.pumpAndSettle(); - -// //pressing enter should trigger the replaceSelected method -// await editor.pressLogicKey( -// key: LogicalKeyboardKey.enter, -// ); -// await tester.pumpAndSettle(); - -// //note our document only has one node -// final textNode = editor.nodeAtPath([0]) as TextNode; -// const expectedText = 'Welcome to Appflowy 😁'; -// expect(textNode.toPlainText(), expectedText); -// }); - -// testWidgets('replace does not change text when no match is found', -// (tester) async { -// const textInputKey = Key('replaceTextField'); -// const pattern = 'Flutter'; - -// final editor = await _prepareWithTextInputForFind( -// tester, -// lines: 1, -// pattern: pattern, -// ); - -// await tester.tap(find.byKey(textInputKey)); -// await tester.enterText(find.byKey(textInputKey), pattern); -// await tester.pumpAndSettle(); -// await tester.testTextInput.receiveAction(TextInputAction.done); -// await tester.pumpAndSettle(); - -// await editor.pressLogicKey( -// key: LogicalKeyboardKey.enter, -// ); -// await tester.pumpAndSettle(); - -// final textNode = editor.nodeAtPath([0]) as TextNode; -// const expectedText = 'Welcome to Appflowy 😁'; -// expect(textNode.toPlainText(), expectedText); -// }); - -// testWidgets('found selected match is replaced properly', (tester) async { -// const patternToBeFound = 'Welcome'; -// const replacePattern = 'Salute'; -// const textInputKey = Key('replaceTextField'); - -// final editor = await _prepareWithTextInputForFind( -// tester, -// lines: 3, -// pattern: patternToBeFound, -// ); - -// //check if matches are not yet replaced -// var textNode = editor.nodeAtPath([2]) as TextNode; -// var expectedText = '$patternToBeFound to Appflowy 😁'; -// expect(textNode.toPlainText(), expectedText); - -// //we select the replace text field and provide replacePattern -// await tester.tap(find.byKey(textInputKey)); -// await tester.enterText(find.byKey(textInputKey), replacePattern); -// await tester.pumpAndSettle(); -// await tester.testTextInput.receiveAction(TextInputAction.done); -// await tester.pumpAndSettle(); - -// await editor.pressLogicKey( -// key: LogicalKeyboardKey.enter, -// ); - -// await tester.pumpAndSettle(); - -// //we know that the findAndHighlight method selects the last -// //matched occurance in the editor document. -// textNode = editor.nodeAtPath([2]) as TextNode; -// expectedText = '$replacePattern to Appflowy 😁'; -// expect(textNode.toPlainText(), expectedText); - -// //also check if other matches are not yet replaced -// textNode = editor.nodeAtPath([1]) as TextNode; -// expectedText = '$patternToBeFound to Appflowy 😁'; -// expect(textNode.toPlainText(), expectedText); -// }); - -// testWidgets('replace all on found matches', (tester) async { -// const patternToBeFound = 'Welcome'; -// const replacePattern = 'Salute'; -// const expectedText = '$replacePattern to Appflowy 😁'; -// const lines = 3; - -// const textInputKey = Key('replaceTextField'); -// const replaceAllBtn = Key('replaceAllButton'); - -// final editor = await _prepareWithTextInputForFind( -// tester, -// lines: lines, -// pattern: patternToBeFound, -// ); - -// //check if matches are not yet replaced -// var textNode = editor.nodeAtPath([2]) as TextNode; -// var originalText = '$patternToBeFound to Appflowy 😁'; -// expect(textNode.toPlainText(), originalText); - -// await tester.tap(find.byKey(textInputKey)); -// await tester.enterText(find.byKey(textInputKey), replacePattern); -// await tester.pumpAndSettle(); -// await tester.testTextInput.receiveAction(TextInputAction.done); -// await tester.pumpAndSettle(); - -// await tester.tap(find.byKey(replaceAllBtn)); -// await tester.pumpAndSettle(); - -// //all matches should be replaced -// for (var i = 0; i < lines; i++) { -// textNode = editor.nodeAtPath([i]) as TextNode; -// expect(textNode.toPlainText(), expectedText); -// } -// }); -// }); -// } - -// Future _prepare( -// WidgetTester tester, { -// int lines = 1, -// }) async { -// const text = 'Welcome to Appflowy 😁'; -// final editor = tester.editor; -// for (var i = 0; i < lines; i++) { -// editor.insertTextNode(text); -// } -// await editor.startTesting(); -// await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); - -// if (Platform.isWindows || Platform.isLinux) { -// await editor.pressLogicKey( -// key: LogicalKeyboardKey.keyH, -// isControlPressed: true, -// ); -// } else { -// await editor.pressLogicKey( -// key: LogicalKeyboardKey.keyH, -// isMetaPressed: true, -// ); -// } - -// await tester.pumpAndSettle(const Duration(milliseconds: 1000)); - -// expect(find.byType(FindMenuWidget), findsOneWidget); - -// return Future.value(editor); -// } - -// Future _prepareWithTextInputForFind( -// WidgetTester tester, { -// int lines = 1, -// String pattern = "Welcome", -// }) async { -// const text = 'Welcome to Appflowy 😁'; -// const textInputKey = Key('findTextField'); -// final editor = tester.editor; -// for (var i = 0; i < lines; i++) { -// editor.insertTextNode(text); -// } -// await editor.startTesting(); -// await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); - -// if (Platform.isWindows || Platform.isLinux) { -// await editor.pressLogicKey( -// key: LogicalKeyboardKey.keyH, -// isControlPressed: true, -// ); -// } else { -// await editor.pressLogicKey( -// key: LogicalKeyboardKey.keyH, -// isMetaPressed: true, -// ); -// } - -// await tester.pumpAndSettle(const Duration(milliseconds: 1000)); - -// expect(find.byType(FindMenuWidget), findsOneWidget); - -// await tester.tap(find.byKey(textInputKey)); -// await tester.enterText(find.byKey(textInputKey), pattern); -// await tester.pumpAndSettle(); -// await tester.testTextInput.receiveAction(TextInputAction.done); -// await tester.pumpAndSettle(); - -// //pressing enter should trigger the findAndHighlight method, which -// //will find the pattern inside the editor. -// await editor.pressLogicKey( -// key: LogicalKeyboardKey.enter, -// ); - -// return Future.value(editor); -// } +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:appflowy_editor/src/editor/find_replace_menu/find_replace_widget.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; + +import '../infra/testable_editor.dart'; +import 'find_replace_menu_utils.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('find_replace_menu.dart replaceMenu', () { + testWidgets('replace menu appears properly', (tester) async { + await prepareFindAndReplaceDialog(tester, openReplace: true); + + //the prepare method only checks if FindMenuWidget is present + //so here we also check if FindMenuWidget contains TextField + //and IconButtons or not. + //and whether there are two textfields for replace menu as well. + expect(find.byType(TextField), findsNWidgets(2)); + expect(find.byType(IconButton), findsAtLeastNWidgets(6)); + }); + + testWidgets('replace menu disappears when close is called', (tester) async { + await prepareFindAndReplaceDialog(tester, openReplace: true); + + await tester.tap(find.byKey(const Key('closeButton'))); + await tester.pumpAndSettle(); + + expect(find.byType(FindMenuWidget), findsNothing); + expect(find.byType(TextField), findsNothing); + expect(find.byType(IconButton), findsNothing); + }); + + testWidgets('replace menu does not work when find is not called', + (tester) async { + const pattern = 'Flutter'; + final editor = tester.editor; + editor.addParagraphs(1, initialText: text); + + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + + await pressFindAndReplaceCommand(editor, openReplace: true); + + await tester.pumpAndSettle(); + + expect(find.byType(FindMenuWidget), findsOneWidget); + + await enterInputIntoFindDialog(tester, pattern, isReplaceField: true); + + await editor.pressKey( + key: LogicalKeyboardKey.enter, + ); + + //if nothing is replaced then the original text will remain as it is + final node = editor.nodeAtPath([0]); + expect(node!.delta!.toPlainText(), text); + + await editor.dispose(); + }); + + testWidgets('replace does not change text when no match is found', + (tester) async { + const pattern = 'Flutter'; + + final editor = tester.editor; + editor.addParagraphs(1, initialText: text); + + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + + await pressFindAndReplaceCommand(editor, openReplace: true); + + await tester.pumpAndSettle(); + expect(find.byType(FindMenuWidget), findsOneWidget); + + //we put the pattern in the find dialog and press enter + await enterInputIntoFindDialog(tester, pattern); + await editor.pressKey( + key: LogicalKeyboardKey.enter, + ); + await tester.pumpAndSettle(); + + //now we input some text into the replace text field and try to replace + await enterInputIntoFindDialog(tester, pattern, isReplaceField: true); + await editor.pressKey( + key: LogicalKeyboardKey.enter, + ); + await tester.pumpAndSettle(); + + final node = editor.nodeAtPath([0]); + expect(node!.delta!.toPlainText(), text); + await editor.dispose(); + }); + + //Before: + //Welcome to Appflowy 😁 + //After: + //Salute to Appflowy 😁 + testWidgets('found selected match is replaced properly', (tester) async { + const patternToBeFound = 'Welcome'; + const replacePattern = 'Salute'; + final expectedText = '$replacePattern${text.substring(7)}'; + + final editor = tester.editor; + editor.addParagraphs(1, initialText: text); + + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + + await pressFindAndReplaceCommand(editor, openReplace: true); + + await tester.pumpAndSettle(); + expect(find.byType(FindMenuWidget), findsOneWidget); + + //we put the pattern in the find dialog and press enter + await enterInputIntoFindDialog(tester, patternToBeFound); + await editor.pressKey( + key: LogicalKeyboardKey.enter, + ); + await tester.pumpAndSettle(); + + //we expect the found pattern to be highlighted + final node = editor.nodeAtPath([0]); + final selection = + Selection.single(path: [0], startOffset: 0, endOffset: text.length); + + checkIfNotHighlighted(node!, selection, expectedResult: false); + + //now we input some text into the replace text field and try to replace + await enterInputIntoFindDialog( + tester, + replacePattern, + isReplaceField: true, + ); + await editor.pressKey( + key: LogicalKeyboardKey.enter, + ); + await tester.pumpAndSettle(); + + expect(node.delta!.toPlainText(), expectedText); + + await editor.dispose(); + }); + + testWidgets('''within multiple matched patterns replace + should only replace the currently selected match''', (tester) async { + const patternToBeFound = 'Welcome'; + const replacePattern = 'Salute'; + final expectedText = '$replacePattern${text.substring(7)}'; + + final editor = tester.editor; + editor.addParagraphs(3, initialText: text); + + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + + await pressFindAndReplaceCommand(editor, openReplace: true); + + await tester.pumpAndSettle(); + expect(find.byType(FindMenuWidget), findsOneWidget); + + //we put the pattern in the find dialog and press enter + await enterInputIntoFindDialog(tester, patternToBeFound); + await editor.pressKey( + key: LogicalKeyboardKey.enter, + ); + await tester.pumpAndSettle(); + + //lets check after find operation, the last match is selected. + checkCurrentSelection(editor, [2], 0, patternToBeFound.length); + + //now we input some text into the replace text field and try to replace + await enterInputIntoFindDialog( + tester, + replacePattern, + isReplaceField: true, + ); + await editor.pressKey( + key: LogicalKeyboardKey.enter, + ); + await tester.pumpAndSettle(); + + //only the node at path 2 should get replaced, all other nodes should stay as before. + final lastNode = editor.nodeAtPath([2]); + expect(lastNode!.delta!.toPlainText(), expectedText); + + final middleNode = editor.nodeAtPath([1]); + expect(middleNode!.delta!.toPlainText(), text); + + final firstNode = editor.nodeAtPath([0]); + expect(firstNode!.delta!.toPlainText(), text); + + await editor.dispose(); + }); + + testWidgets('replace all on found matches', (tester) async { + const patternToBeFound = 'Welcome'; + const replacePattern = 'Salute'; + final expectedText = '$replacePattern${text.substring(7)}'; + const replaceAllBtn = Key('replaceAllButton'); + const lines = 3; + + final editor = tester.editor; + editor.addParagraphs(lines, initialText: text); + + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + + await pressFindAndReplaceCommand(editor, openReplace: true); + + await tester.pumpAndSettle(); + expect(find.byType(FindMenuWidget), findsOneWidget); + + //we put the pattern in the find dialog and press enter + await enterInputIntoFindDialog(tester, patternToBeFound); + await editor.pressKey( + key: LogicalKeyboardKey.enter, + ); + await tester.pumpAndSettle(); + + //now we input some text into the replace text field and try replace all + await enterInputIntoFindDialog( + tester, + replacePattern, + isReplaceField: true, + ); + + await tester.tap(find.byKey(replaceAllBtn)); + await tester.pumpAndSettle(); + + //all matches should be replaced + for (var i = 0; i < lines; i++) { + final node = editor.nodeAtPath([i]); + expect(node!.delta!.toPlainText(), expectedText); + } + await editor.dispose(); + }); + }); +} diff --git a/test/new/find_replace_menu/find_replace_menu_utils.dart b/test/new/find_replace_menu/find_replace_menu_utils.dart new file mode 100644 index 000000000..5a14b858c --- /dev/null +++ b/test/new/find_replace_menu/find_replace_menu_utils.dart @@ -0,0 +1,86 @@ +import 'dart:io'; + +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/editor/find_replace_menu/find_replace_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../infra/testable_editor.dart'; + +const text = 'Welcome to Appflowy 😁'; + +Future prepareFindAndReplaceDialog( + WidgetTester tester, { + bool openReplace = false, +}) async { + final editor = tester.editor; + editor.addParagraphs(3, initialText: text); + + await editor.startTesting(); + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + + await pressFindAndReplaceCommand(editor, openReplace: openReplace); + + await tester.pumpAndSettle(const Duration(milliseconds: 1000)); + + expect(find.byType(FindMenuWidget), findsOneWidget); +} + +Future enterInputIntoFindDialog( + WidgetTester tester, + String pattern, { + bool isReplaceField = false, +}) async { + final textInputKey = isReplaceField + ? const Key('replaceTextField') + : const Key('findTextField'); + await tester.tap(find.byKey(textInputKey)); + await tester.enterText(find.byKey(textInputKey), pattern); + await tester.pumpAndSettle(); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); +} + +Future pressFindAndReplaceCommand( + TestableEditor editor, { + bool openReplace = false, +}) async { + await editor.pressKey( + key: openReplace ? LogicalKeyboardKey.keyH : LogicalKeyboardKey.keyF, + isMetaPressed: Platform.isMacOS, + isControlPressed: !Platform.isMacOS, + ); +} + +void checkIfNotHighlighted( + Node node, + Selection selection, { + bool expectedResult = true, +}) { + //if the expectedResult is true: + //we expect that nothing is highlighted in our current document. + //otherwise: we expect that something is highlighted. + expect( + node.allSatisfyInSelection(selection, (delta) { + return delta.whereType().every( + (el) => el.attributes?[AppFlowyRichTextKeys.highlightColor] == null, + ); + }), + expectedResult, + ); +} + +void checkCurrentSelection( + TestableEditor editor, + Path path, + int startOffset, + int endOffset, +) { + final selection = + editor.editorState.service.selectionService.currentSelection.value; + + expect(selection != null, true); + expect(selection!.start, Position(path: path, offset: startOffset)); + expect(selection.end, Position(path: path, offset: endOffset)); +} From 719006c82aefd7b80eee6a3b1e291a765d5c453c Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Mon, 24 Jul 2023 14:38:31 +0200 Subject: [PATCH 35/45] fix: localizations + resolve scroll bug partially --- example/windows/runner/CMakeLists.txt | 7 + example/windows/runner/Runner.rc | 10 +- .../standard_block_components.dart | 11 +- .../service/scroll_service_widget.dart | 12 +- .../selection/desktop_selection_service.dart | 4 +- .../find_replace_command.dart | 74 ++++++--- .../find_replace_menu/find_menu_service.dart | 3 + .../find_replace_widget.dart | 47 ++++-- .../find_replace_menu/search_service.dart | 145 ++++++++++-------- lib/src/editor_state.dart | 2 + 10 files changed, 206 insertions(+), 109 deletions(-) diff --git a/example/windows/runner/CMakeLists.txt b/example/windows/runner/CMakeLists.txt index c16efd198..5e69c2b90 100644 --- a/example/windows/runner/CMakeLists.txt +++ b/example/windows/runner/CMakeLists.txt @@ -20,6 +20,13 @@ add_executable(${BINARY_NAME} WIN32 # that need different build settings. apply_standard_settings(${BINARY_NAME}) +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + # Disable Windows macros that collide with C++ standard library functions. target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") diff --git a/example/windows/runner/Runner.rc b/example/windows/runner/Runner.rc index fa4251082..54114569a 100644 --- a/example/windows/runner/Runner.rc +++ b/example/windows/runner/Runner.rc @@ -60,14 +60,14 @@ IDI_APP_ICON ICON "resources\\app_icon.ico" // Version // -#ifdef FLUTTER_BUILD_NUMBER -#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD #else -#define VERSION_AS_NUMBER 1,0,0 +#define VERSION_AS_NUMBER 1,0,0,0 #endif -#ifdef FLUTTER_BUILD_NAME -#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION #else #define VERSION_AS_STRING "1.0.0" #endif diff --git a/lib/src/editor/block_component/standard_block_components.dart b/lib/src/editor/block_component/standard_block_components.dart index 3e75d1ddc..09d0bba5e 100644 --- a/lib/src/editor/block_component/standard_block_components.dart +++ b/lib/src/editor/block_component/standard_block_components.dart @@ -118,7 +118,16 @@ final List standardCommandShortcutEvents = [ outdentCommand, // - ...findAndReplaceCommands, + ...findAndReplaceCommands( + FindReplaceLocalizations( + find: 'Find', + previousMatch: 'Previous match', + nextMatch: 'Next match', + close: 'Close', + replace: 'Replace', + replaceAll: 'Replace all', + ), + ), // exitEditingCommand, diff --git a/lib/src/editor/editor_component/service/scroll_service_widget.dart b/lib/src/editor/editor_component/service/scroll_service_widget.dart index 99bb8d99c..ef045cd18 100644 --- a/lib/src/editor/editor_component/service/scroll_service_widget.dart +++ b/lib/src/editor/editor_component/service/scroll_service_widget.dart @@ -104,15 +104,25 @@ class _ScrollServiceWidgetState extends State // should auto scroll after the cursor or selection updated. final selection = editorState.selection; if (selection == null || - editorState.selectionUpdateReason == SelectionUpdateReason.selectAll) { + [SelectionUpdateReason.selectAll, SelectionUpdateReason.searchHighlight] + .contains(editorState.selectionUpdateReason)) { return; } + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { final selectionRect = editorState.selectionRects(); if (selectionRect.isEmpty) { return; } + final endTouchPoint = selectionRect.last.centerRight; + + if (editorState.selectionUpdateReason == + SelectionUpdateReason.searchNavigate) { + scrollController.jumpTo(endTouchPoint.dy - 100); + return; + } + if (selection.isCollapsed) { if (PlatformExtension.isMobile) { // soft keyboard diff --git a/lib/src/editor/editor_component/service/selection/desktop_selection_service.dart b/lib/src/editor/editor_component/service/selection/desktop_selection_service.dart index e7dfee8d3..837f38ac1 100644 --- a/lib/src/editor/editor_component/service/selection/desktop_selection_service.dart +++ b/lib/src/editor/editor_component/service/selection/desktop_selection_service.dart @@ -128,9 +128,11 @@ class _DesktopSelectionServiceWidgetState void _updateSelection() { final selection = editorState.selection; + // TODO: why do we need to check this? if (currentSelection.value == selection && - editorState.selectionUpdateReason == SelectionUpdateReason.uiEvent && + [SelectionUpdateReason.uiEvent, SelectionUpdateReason.searchHighlight] + .contains(editorState.selectionUpdateReason) && editorState.selectionType != SelectionType.block) { return; } diff --git a/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart b/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart index 77768467f..00a0d2c71 100644 --- a/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart +++ b/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart @@ -2,10 +2,31 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/editor/find_replace_menu/find_menu_service.dart'; import 'package:flutter/material.dart'; -final List findAndReplaceCommands = [ - openFindDialog, - openReplaceDialog, -]; +List findAndReplaceCommands( + FindReplaceLocalizations localizations, +) => + [ + openFindDialog(localizations: localizations), + openReplaceDialog(localizations: localizations), + ]; + +class FindReplaceLocalizations { + FindReplaceLocalizations({ + required this.find, + required this.previousMatch, + required this.nextMatch, + required this.close, + required this.replace, + required this.replaceAll, + }); + + final String find; + final String previousMatch; + final String nextMatch; + final String close; + final String replace; + final String replaceAll; +} /// Show the slash menu /// @@ -13,28 +34,37 @@ final List findAndReplaceCommands = [ /// - desktop /// - web /// -final CommandShortcutEvent openFindDialog = CommandShortcutEvent( - key: 'show the find dialog', - command: 'ctrl+f', - macOSCommand: 'cmd+f', - handler: (editorState) => _showFindAndReplaceDialog( - editorState, - ), -); +CommandShortcutEvent openFindDialog({ + required FindReplaceLocalizations localizations, +}) => + CommandShortcutEvent( + key: 'show the find dialog', + command: 'ctrl+f', + macOSCommand: 'cmd+f', + handler: (editorState) => _showFindAndReplaceDialog( + editorState, + localizations: localizations, + ), + ); -final CommandShortcutEvent openReplaceDialog = CommandShortcutEvent( - key: 'show the find and replace dialog', - command: 'ctrl+h', - macOSCommand: 'cmd+h', - handler: (editorState) => _showFindAndReplaceDialog( - editorState, - openReplace: true, - ), -); +CommandShortcutEvent openReplaceDialog({ + required FindReplaceLocalizations localizations, +}) => + CommandShortcutEvent( + key: 'show the find and replace dialog', + command: 'ctrl+h', + macOSCommand: 'cmd+h', + handler: (editorState) => _showFindAndReplaceDialog( + editorState, + localizations: localizations, + openReplace: true, + ), + ); FindReplaceService? _findReplaceService; KeyEventResult _showFindAndReplaceDialog( EditorState editorState, { + required FindReplaceLocalizations localizations, bool openReplace = false, }) { if (PlatformExtension.isMobile) { @@ -67,7 +97,9 @@ KeyEventResult _showFindAndReplaceDialog( context: context, editorState: editorState, replaceFlag: openReplace, + localizations: localizations, ); + _findReplaceService?.show(); } }(); diff --git a/lib/src/editor/find_replace_menu/find_menu_service.dart b/lib/src/editor/find_replace_menu/find_menu_service.dart index cd8b14aca..7c4259939 100644 --- a/lib/src/editor/find_replace_menu/find_menu_service.dart +++ b/lib/src/editor/find_replace_menu/find_menu_service.dart @@ -15,11 +15,13 @@ class FindReplaceMenu implements FindReplaceService { required this.context, required this.editorState, required this.replaceFlag, + required this.localizations, }); final BuildContext context; final EditorState editorState; final bool replaceFlag; + final FindReplaceLocalizations localizations; final double topOffset = 52; final double rightOffset = 40; @@ -78,6 +80,7 @@ class FindReplaceMenu implements FindReplaceService { dismiss: dismiss, editorState: editorState, replaceFlag: replaceFlag, + localizations: localizations, ), ), ), diff --git a/lib/src/editor/find_replace_menu/find_replace_widget.dart b/lib/src/editor/find_replace_menu/find_replace_widget.dart index 67b790f9f..5e263f15d 100644 --- a/lib/src/editor/find_replace_menu/find_replace_widget.dart +++ b/lib/src/editor/find_replace_menu/find_replace_widget.dart @@ -10,17 +10,20 @@ class FindMenuWidget extends StatefulWidget { required this.dismiss, required this.editorState, required this.replaceFlag, + required this.localizations, }); final VoidCallback dismiss; final EditorState editorState; final bool replaceFlag; + final FindReplaceLocalizations localizations; @override State createState() => _FindMenuWidgetState(); } class _FindMenuWidgetState extends State { + final focusNode = FocusNode(); final findController = TextEditingController(); final replaceController = TextEditingController(); String queriedPattern = ''; @@ -34,6 +37,19 @@ class _FindMenuWidgetState extends State { searchService = SearchService( editorState: widget.editorState, ); + + WidgetsBinding.instance.addPostFrameCallback((_) { + focusNode.requestFocus(); + }); + + findController.addListener(_searchPattern); + } + + @override + void dispose() { + findController.removeListener(_searchPattern); + focusNode.dispose(); + super.dispose(); } @override @@ -55,25 +71,31 @@ class _FindMenuWidgetState extends State { height: 30, child: TextField( key: const Key('findTextField'), - autofocus: true, + focusNode: focusNode, controller: findController, - onSubmitted: (_) => _searchPattern(), - decoration: _buildInputDecoration("Find"), + onSubmitted: (_) => searchService.navigateToMatch(), + decoration: _buildInputDecoration(widget.localizations.find), ), ), IconButton( key: const Key('previousMatchButton'), iconSize: _iconSize, - onPressed: () => searchService.navigateToMatch(moveUp: true), + onPressed: () { + searchService.navigateToMatch(moveUp: true); + focusNode.requestFocus(); + }, icon: const Icon(Icons.arrow_upward), - tooltip: 'Previous Match', + tooltip: widget.localizations.previousMatch, ), IconButton( key: const Key('nextMatchButton'), iconSize: _iconSize, - onPressed: () => searchService.navigateToMatch(), + onPressed: () { + searchService.navigateToMatch(); + focusNode.requestFocus(); + }, icon: const Icon(Icons.arrow_downward), - tooltip: 'Next Match', + tooltip: widget.localizations.nextMatch, ), IconButton( key: const Key('closeButton'), @@ -84,10 +106,10 @@ class _FindMenuWidgetState extends State { queriedPattern, unhighlight: true, ); - setState(() => queriedPattern = ''); + queriedPattern = ''; }, icon: const Icon(Icons.close), - tooltip: 'Close', + tooltip: widget.localizations.close, ), ], ), @@ -102,21 +124,22 @@ class _FindMenuWidgetState extends State { autofocus: false, controller: replaceController, onSubmitted: (_) => _replaceSelectedWord(), - decoration: _buildInputDecoration("Replace"), + decoration: + _buildInputDecoration(widget.localizations.replace), ), ), IconButton( onPressed: () => _replaceSelectedWord(), icon: const Icon(Icons.find_replace), iconSize: _iconSize, - tooltip: 'Replace', + tooltip: widget.localizations.replace, ), IconButton( key: const Key('replaceAllButton'), onPressed: () => _replaceAllMatches(), icon: const Icon(Icons.change_circle_outlined), iconSize: _iconSize, - tooltip: 'Replace All', + tooltip: widget.localizations.replaceAll, ), ], ) diff --git a/lib/src/editor/find_replace_menu/search_service.dart b/lib/src/editor/find_replace_menu/search_service.dart index e8734e5c6..7ddcb7ca2 100644 --- a/lib/src/editor/find_replace_menu/search_service.dart +++ b/lib/src/editor/find_replace_menu/search_service.dart @@ -1,5 +1,6 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/editor/find_replace_menu/search_algorithm.dart'; +import 'package:flutter/foundation.dart'; const foundSelectedColor = '0x6000BCF0'; @@ -50,7 +51,79 @@ class SearchService { ); } - selectedIndex = matchedPositions.length - 1; + selectedIndex = 0; + } + + List _getAllTextNodes() { + final contents = editorState.document.root.children; + + if (contents.isEmpty) return []; + + final firstNode = contents.firstWhere( + (el) => el.delta != null, + ); + + final lastNode = contents.lastWhere( + (el) => el.delta != null, + ); + + //iterate within all the text nodes of the document. + final nodes = NodeIterator( + document: editorState.document, + startNode: firstNode, + endNode: lastNode, + ).toList(); + + nodes.removeWhere((node) => node.delta == null); + + return nodes; + } + + /// This method takes in the TextNode's path, matches is a list of offsets, + /// patternLength is the length of the word which is being searched. + /// + /// So for example: path= 1, offset= 10, and patternLength= 5 will mean + /// that the word is located on path 1 from [1,10] to [1,14] + void _highlightMatches( + Path path, + List matches, + int patternLength, { + bool unhighlight = false, + }) { + for (final match in matches) { + Position start = Position(path: path, offset: match); + _selectWordAtPosition(start); + + if (unhighlight) { + final selection = editorState.selection!; + editorState.formatDelta( + selection, + {AppFlowyRichTextKeys.highlightColor: null}, + ); + } else { + formatHighlightColor( + editorState, + foundSelectedColor, + ); + } + editorState.undoManager.forgetRecentUndo(); + } + } + + void _selectWordAtPosition(Position start, [bool isNavigating = false]) { + debugPrint("REACHED HERE: $isNavigating"); + + Position end = Position( + path: start.path, + offset: start.offset + queriedPattern.length, + ); + + editorState.updateSelectionWithReason( + Selection(start: start, end: end), + reason: isNavigating + ? SelectionUpdateReason.searchNavigate + : SelectionUpdateReason.searchHighlight, + ); } /// This method takes in a boolean parameter moveUp, if set to true, @@ -58,18 +131,19 @@ class SearchService { /// Otherwise the match below the current selected match is newly selected. void navigateToMatch({bool moveUp = false}) { if (matchedPositions.isEmpty) return; + if (moveUp) { selectedIndex = selectedIndex - 1 < 0 ? matchedPositions.length - 1 : --selectedIndex; Position match = matchedPositions[selectedIndex]; - _selectWordAtPosition(match); + _selectWordAtPosition(match, true); } else { selectedIndex = (selectedIndex + 1) < matchedPositions.length ? ++selectedIndex : 0; final match = matchedPositions[selectedIndex]; - _selectWordAtPosition(match); + _selectWordAtPosition(match, true); } } @@ -126,69 +200,4 @@ class SearchService { replaceSelectedWord(replaceText); } } - - /// This method takes in the TextNode's path, matches is a list of offsets, - /// patternLength is the length of the word which is being searched. - /// - /// So for example: path= 1, offset= 10, and patternLength= 5 will mean - /// that the word is located on path 1 from [1,10] to [1,14] - void _highlightMatches( - Path path, - List matches, - int patternLength, { - bool unhighlight = false, - }) { - for (final match in matches) { - Position start = Position(path: path, offset: match); - _selectWordAtPosition(start); - - if (unhighlight) { - final selection = editorState.selection!; - editorState.formatDelta( - selection, - {AppFlowyRichTextKeys.highlightColor: null}, - ); - } else { - formatHighlightColor( - editorState, - foundSelectedColor, - ); - } - editorState.undoManager.forgetRecentUndo(); - } - } - - void _selectWordAtPosition(Position start) { - Position end = Position( - path: start.path, - offset: start.offset + queriedPattern.length, - ); - - editorState.updateSelectionWithReason(Selection(start: start, end: end)); - } - - List _getAllTextNodes() { - final contents = editorState.document.root.children; - - if (contents.isEmpty) return []; - - final firstNode = contents.firstWhere( - (el) => el.delta != null, - ); - - final lastNode = contents.lastWhere( - (el) => el.delta != null, - ); - - //iterate within all the text nodes of the document. - final nodes = NodeIterator( - document: editorState.document, - startNode: firstNode, - endNode: lastNode, - ).toList(); - - nodes.removeWhere((node) => node.delta == null); - - return nodes; - } } diff --git a/lib/src/editor_state.dart b/lib/src/editor_state.dart index 87d50749e..1c7b3a73b 100644 --- a/lib/src/editor_state.dart +++ b/lib/src/editor_state.dart @@ -27,6 +27,8 @@ enum SelectionUpdateReason { uiEvent, // like mouse click, keyboard event transaction, // like insert, delete, format selectAll, + searchHighlight, // Highlighting search results + searchNavigate, // Navigate to a search result } enum SelectionType { From 04946fb4d4e0617d4cfef2ed2562f154d11c40f9 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Mon, 24 Jul 2023 16:54:23 +0200 Subject: [PATCH 36/45] fix: without update selection on highlight --- lib/src/editor/command/text_commands.dart | 11 +++++++++-- .../editor/find_replace_menu/find_menu_service.dart | 1 - lib/src/editor/find_replace_menu/search_service.dart | 3 --- lib/src/editor/toolbar/utils/format_color.dart | 2 ++ lib/src/editor_state.dart | 4 +--- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/src/editor/command/text_commands.dart b/lib/src/editor/command/text_commands.dart index 352bd83f6..da05e17e6 100644 --- a/lib/src/editor/command/text_commands.dart +++ b/lib/src/editor/command/text_commands.dart @@ -132,7 +132,11 @@ extension TextTransforms on EditorState { /// format the delta at the given selection. /// /// If the [Selection] is not passed in, use the current selection. - Future formatDelta(Selection? selection, Attributes attributes) async { + Future formatDelta( + Selection? selection, + Attributes attributes, [ + bool withUpdateSelection = true, + ]) async { selection ??= this.selection; selection = selection?.normalized; @@ -164,7 +168,10 @@ extension TextTransforms on EditorState { ..afterSelection = transaction.beforeSelection; } - return apply(transaction); + return apply( + transaction, + withUpdateSelection: withUpdateSelection, + ); } /// Toggles the given attribute on or off for the selected text. diff --git a/lib/src/editor/find_replace_menu/find_menu_service.dart b/lib/src/editor/find_replace_menu/find_menu_service.dart index 7c4259939..fc820b3a3 100644 --- a/lib/src/editor/find_replace_menu/find_menu_service.dart +++ b/lib/src/editor/find_replace_menu/find_menu_service.dart @@ -1,7 +1,6 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/editor/find_replace_menu/find_replace_widget.dart'; import 'package:flutter/material.dart'; -import '../../editor_state.dart'; abstract class FindReplaceService { void show(); diff --git a/lib/src/editor/find_replace_menu/search_service.dart b/lib/src/editor/find_replace_menu/search_service.dart index 7ddcb7ca2..e8087d03c 100644 --- a/lib/src/editor/find_replace_menu/search_service.dart +++ b/lib/src/editor/find_replace_menu/search_service.dart @@ -1,6 +1,5 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/editor/find_replace_menu/search_algorithm.dart'; -import 'package:flutter/foundation.dart'; const foundSelectedColor = '0x6000BCF0'; @@ -111,8 +110,6 @@ class SearchService { } void _selectWordAtPosition(Position start, [bool isNavigating = false]) { - debugPrint("REACHED HERE: $isNavigating"); - Position end = Position( path: start.path, offset: start.offset + queriedPattern.length, diff --git a/lib/src/editor/toolbar/utils/format_color.dart b/lib/src/editor/toolbar/utils/format_color.dart index 51f7763be..580491a4e 100644 --- a/lib/src/editor/toolbar/utils/format_color.dart +++ b/lib/src/editor/toolbar/utils/format_color.dart @@ -4,6 +4,7 @@ void formatHighlightColor(EditorState editorState, String color) { editorState.formatDelta( editorState.selection, {AppFlowyRichTextKeys.highlightColor: color}, + false, ); } @@ -11,5 +12,6 @@ void formatFontColor(EditorState editorState, String color) { editorState.formatDelta( editorState.selection, {AppFlowyRichTextKeys.textColor: color}, + false, ); } diff --git a/lib/src/editor_state.dart b/lib/src/editor_state.dart index 1c7b3a73b..44f35ff99 100644 --- a/lib/src/editor_state.dart +++ b/lib/src/editor_state.dart @@ -244,9 +244,7 @@ class EditorState { } // TODO: execute this line after the UI has been updated. - { - completer.complete(); - } + completer.complete(); return completer.future; } From 8f397bdaa4e5b67b5c9e215f5bdcdda5337eb8e4 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Tue, 25 Jul 2023 11:32:26 +0200 Subject: [PATCH 37/45] fix: do not select words when highlighting --- .../editor/find_replace_menu/search_service.dart | 11 ++++++++--- .../toolbar/desktop/items/color/color_menu.dart | 2 ++ .../color/background_color_options_widgets.dart | 1 + .../color/text_color_options_widgets.dart | 1 + lib/src/editor/toolbar/utils/format_color.dart | 14 +++++++++++--- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/lib/src/editor/find_replace_menu/search_service.dart b/lib/src/editor/find_replace_menu/search_service.dart index e8087d03c..46f116145 100644 --- a/lib/src/editor/find_replace_menu/search_service.dart +++ b/lib/src/editor/find_replace_menu/search_service.dart @@ -90,11 +90,15 @@ class SearchService { bool unhighlight = false, }) { for (final match in matches) { - Position start = Position(path: path, offset: match); - _selectWordAtPosition(start); + final start = Position(path: path, offset: match); + final end = Position( + path: start.path, + offset: start.offset + queriedPattern.length, + ); + + final selection = Selection(start: start, end: end); if (unhighlight) { - final selection = editorState.selection!; editorState.formatDelta( selection, {AppFlowyRichTextKeys.highlightColor: null}, @@ -102,6 +106,7 @@ class SearchService { } else { formatHighlightColor( editorState, + selection, foundSelectedColor, ); } diff --git a/lib/src/editor/toolbar/desktop/items/color/color_menu.dart b/lib/src/editor/toolbar/desktop/items/color/color_menu.dart index b739d8890..3601433da 100644 --- a/lib/src/editor/toolbar/desktop/items/color/color_menu.dart +++ b/lib/src/editor/toolbar/desktop/items/color/color_menu.dart @@ -52,10 +52,12 @@ void showColorMenu( isTextColor ? formatFontColor( editorState, + editorState.selection, color, ) : formatHighlightColor( editorState, + editorState.selection, color, ); dismissOverlay(); diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/color/background_color_options_widgets.dart b/lib/src/editor/toolbar/mobile/toolbar_items/color/background_color_options_widgets.dart index 404da23ac..cee3a4220 100644 --- a/lib/src/editor/toolbar/mobile/toolbar_items/color/background_color_options_widgets.dart +++ b/lib/src/editor/toolbar/mobile/toolbar_items/color/background_color_options_widgets.dart @@ -72,6 +72,7 @@ class _BackgroundColorOptionsWidgetsState setState(() { formatHighlightColor( widget.editorState, + widget.editorState.selection, e.colorHex, ); }); diff --git a/lib/src/editor/toolbar/mobile/toolbar_items/color/text_color_options_widgets.dart b/lib/src/editor/toolbar/mobile/toolbar_items/color/text_color_options_widgets.dart index 2888fd07a..87a22cc5a 100644 --- a/lib/src/editor/toolbar/mobile/toolbar_items/color/text_color_options_widgets.dart +++ b/lib/src/editor/toolbar/mobile/toolbar_items/color/text_color_options_widgets.dart @@ -70,6 +70,7 @@ class _TextColorOptionsWidgetsState extends State { setState(() { formatFontColor( widget.editorState, + widget.editorState.selection, e.colorHex, ); }); diff --git a/lib/src/editor/toolbar/utils/format_color.dart b/lib/src/editor/toolbar/utils/format_color.dart index 580491a4e..2c158d371 100644 --- a/lib/src/editor/toolbar/utils/format_color.dart +++ b/lib/src/editor/toolbar/utils/format_color.dart @@ -1,14 +1,22 @@ import 'package:appflowy_editor/appflowy_editor.dart'; -void formatHighlightColor(EditorState editorState, String color) { +void formatHighlightColor( + EditorState editorState, + Selection? selection, + String color, +) { editorState.formatDelta( - editorState.selection, + selection, {AppFlowyRichTextKeys.highlightColor: color}, false, ); } -void formatFontColor(EditorState editorState, String color) { +void formatFontColor( + EditorState editorState, + Selection? selection, + String color, +) { editorState.formatDelta( editorState.selection, {AppFlowyRichTextKeys.textColor: color}, From 51d6a0cc63e674465542c19168c6989206f9ae14 Mon Sep 17 00:00:00 2001 From: MayurSMahajan Date: Thu, 27 Jul 2023 15:53:13 +0530 Subject: [PATCH 38/45] test: update expected selection --- .../find_replace_menu_find_test.dart | 13 +++++-------- .../find_replace_menu_replace_test.dart | 8 ++++---- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/test/new/find_replace_menu/find_replace_menu_find_test.dart b/test/new/find_replace_menu/find_replace_menu_find_test.dart index 16104802a..69184250e 100644 --- a/test/new/find_replace_menu/find_replace_menu_find_test.dart +++ b/test/new/find_replace_menu/find_replace_menu_find_test.dart @@ -73,19 +73,16 @@ void main() async { await enterInputIntoFindDialog(tester, pattern); - await editor.pressKey( - key: LogicalKeyboardKey.enter, - ); - //checking if current selection consists an occurance of matched pattern. final selection = editor.editorState.service.selectionService.currentSelection.value; - //we expect the last occurance of the pattern to be found and selected, - //thus that should be the current selection. + //we expect the second occurance of the pattern to be found and selected, + //this is because we send a testTextInput.receiveAction(TextInputAction.done) + //event during submitting our text input, thus the second match is selected. expect(selection != null, true); - expect(selection!.start, Position(path: [2], offset: 0)); - expect(selection.end, Position(path: [2], offset: pattern.length)); + expect(selection!.start, Position(path: [1], offset: 0)); + expect(selection.end, Position(path: [1], offset: pattern.length)); await editor.dispose(); }); diff --git a/test/new/find_replace_menu/find_replace_menu_replace_test.dart b/test/new/find_replace_menu/find_replace_menu_replace_test.dart index 6988e5028..ed2d76f20 100644 --- a/test/new/find_replace_menu/find_replace_menu_replace_test.dart +++ b/test/new/find_replace_menu/find_replace_menu_replace_test.dart @@ -172,8 +172,8 @@ void main() async { ); await tester.pumpAndSettle(); - //lets check after find operation, the last match is selected. - checkCurrentSelection(editor, [2], 0, patternToBeFound.length); + //lets check after find operation, the second match is selected. + checkCurrentSelection(editor, [1], 0, patternToBeFound.length); //now we input some text into the replace text field and try to replace await enterInputIntoFindDialog( @@ -187,10 +187,10 @@ void main() async { await tester.pumpAndSettle(); //only the node at path 2 should get replaced, all other nodes should stay as before. - final lastNode = editor.nodeAtPath([2]); + final lastNode = editor.nodeAtPath([1]); expect(lastNode!.delta!.toPlainText(), expectedText); - final middleNode = editor.nodeAtPath([1]); + final middleNode = editor.nodeAtPath([2]); expect(middleNode!.delta!.toPlainText(), text); final firstNode = editor.nodeAtPath([0]); From da3ce2caf7518eedb77011ab347d2e5a2d11f3f2 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Thu, 27 Jul 2023 13:14:45 +0200 Subject: [PATCH 39/45] fix: matches and styling --- example/lib/pages/editor.dart | 14 ++++++++- .../standard_block_components.dart | 12 ------- .../find_replace_command.dart | 31 ++++++++++++++++--- .../find_replace_menu/find_menu_service.dart | 4 +++ .../find_replace_widget.dart | 13 +++----- .../find_replace_menu/search_service.dart | 31 ++++++++++++++----- 6 files changed, 72 insertions(+), 33 deletions(-) diff --git a/example/lib/pages/editor.dart b/example/lib/pages/editor.dart index 45cb2965d..bd5779727 100644 --- a/example/lib/pages/editor.dart +++ b/example/lib/pages/editor.dart @@ -124,7 +124,19 @@ class Editor extends StatelessWidget { editorState: editorState, scrollController: scrollController, blockComponentBuilders: customBlockComponentBuilders, - commandShortcutEvents: standardCommandShortcutEvents, + commandShortcutEvents: [ + ...standardCommandShortcutEvents, + ...findAndReplaceCommands( + localizations: FindReplaceLocalizations( + find: 'Find', + previousMatch: 'Previous match', + nextMatch: 'Next match', + close: 'Close', + replace: 'Replace', + replaceAll: 'Replace all', + ), + ), + ], characterShortcutEvents: standardCharacterShortcutEvents, ); } diff --git a/lib/src/editor/block_component/standard_block_components.dart b/lib/src/editor/block_component/standard_block_components.dart index 73ef9379c..73b8c8cbd 100644 --- a/lib/src/editor/block_component/standard_block_components.dart +++ b/lib/src/editor/block_component/standard_block_components.dart @@ -117,18 +117,6 @@ final List standardCommandShortcutEvents = [ indentCommand, outdentCommand, - // - ...findAndReplaceCommands( - FindReplaceLocalizations( - find: 'Find', - previousMatch: 'Previous match', - nextMatch: 'Next match', - close: 'Close', - replace: 'Replace', - replaceAll: 'Replace all', - ), - ), - // exitEditingCommand, diff --git a/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart b/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart index 00a0d2c71..d0631e5a3 100644 --- a/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart +++ b/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart @@ -2,14 +2,29 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/editor/find_replace_menu/find_menu_service.dart'; import 'package:flutter/material.dart'; -List findAndReplaceCommands( - FindReplaceLocalizations localizations, -) => +List findAndReplaceCommands({ + required FindReplaceLocalizations localizations, + FindReplaceStyle? style, +}) => [ - openFindDialog(localizations: localizations), - openReplaceDialog(localizations: localizations), + openFindDialog( + localizations: localizations, + style: style ?? FindReplaceStyle(), + ), + openReplaceDialog( + localizations: localizations, + style: style ?? FindReplaceStyle(), + ), ]; +class FindReplaceStyle { + FindReplaceStyle({ + this.highlightColor = const Color(0x6000BCF0), + }); + + final Color highlightColor; +} + class FindReplaceLocalizations { FindReplaceLocalizations({ required this.find, @@ -36,6 +51,7 @@ class FindReplaceLocalizations { /// CommandShortcutEvent openFindDialog({ required FindReplaceLocalizations localizations, + required FindReplaceStyle style, }) => CommandShortcutEvent( key: 'show the find dialog', @@ -44,11 +60,13 @@ CommandShortcutEvent openFindDialog({ handler: (editorState) => _showFindAndReplaceDialog( editorState, localizations: localizations, + style: style, ), ); CommandShortcutEvent openReplaceDialog({ required FindReplaceLocalizations localizations, + required FindReplaceStyle style, }) => CommandShortcutEvent( key: 'show the find and replace dialog', @@ -57,6 +75,7 @@ CommandShortcutEvent openReplaceDialog({ handler: (editorState) => _showFindAndReplaceDialog( editorState, localizations: localizations, + style: style, openReplace: true, ), ); @@ -65,6 +84,7 @@ FindReplaceService? _findReplaceService; KeyEventResult _showFindAndReplaceDialog( EditorState editorState, { required FindReplaceLocalizations localizations, + required FindReplaceStyle style, bool openReplace = false, }) { if (PlatformExtension.isMobile) { @@ -98,6 +118,7 @@ KeyEventResult _showFindAndReplaceDialog( editorState: editorState, replaceFlag: openReplace, localizations: localizations, + style: style, ); _findReplaceService?.show(); diff --git a/lib/src/editor/find_replace_menu/find_menu_service.dart b/lib/src/editor/find_replace_menu/find_menu_service.dart index fc820b3a3..669e16cba 100644 --- a/lib/src/editor/find_replace_menu/find_menu_service.dart +++ b/lib/src/editor/find_replace_menu/find_menu_service.dart @@ -15,12 +15,15 @@ class FindReplaceMenu implements FindReplaceService { required this.editorState, required this.replaceFlag, required this.localizations, + required this.style, }); final BuildContext context; final EditorState editorState; final bool replaceFlag; final FindReplaceLocalizations localizations; + final FindReplaceStyle style; + final double topOffset = 52; final double rightOffset = 40; @@ -80,6 +83,7 @@ class FindReplaceMenu implements FindReplaceService { editorState: editorState, replaceFlag: replaceFlag, localizations: localizations, + style: style, ), ), ), diff --git a/lib/src/editor/find_replace_menu/find_replace_widget.dart b/lib/src/editor/find_replace_menu/find_replace_widget.dart index 5e263f15d..db4693a25 100644 --- a/lib/src/editor/find_replace_menu/find_replace_widget.dart +++ b/lib/src/editor/find_replace_menu/find_replace_widget.dart @@ -11,12 +11,14 @@ class FindMenuWidget extends StatefulWidget { required this.editorState, required this.replaceFlag, required this.localizations, + required this.style, }); final VoidCallback dismiss; final EditorState editorState; final bool replaceFlag; final FindReplaceLocalizations localizations; + final FindReplaceStyle style; @override State createState() => _FindMenuWidgetState(); @@ -36,6 +38,7 @@ class _FindMenuWidgetState extends State { replaceFlag = widget.replaceFlag; searchService = SearchService( editorState: widget.editorState, + style: SearchStyle(highlightColor: widget.style.highlightColor), ); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -80,20 +83,14 @@ class _FindMenuWidgetState extends State { IconButton( key: const Key('previousMatchButton'), iconSize: _iconSize, - onPressed: () { - searchService.navigateToMatch(moveUp: true); - focusNode.requestFocus(); - }, + onPressed: () => searchService.navigateToMatch(moveUp: true), icon: const Icon(Icons.arrow_upward), tooltip: widget.localizations.previousMatch, ), IconButton( key: const Key('nextMatchButton'), iconSize: _iconSize, - onPressed: () { - searchService.navigateToMatch(); - focusNode.requestFocus(); - }, + onPressed: () => searchService.navigateToMatch(), icon: const Icon(Icons.arrow_downward), tooltip: widget.localizations.nextMatch, ), diff --git a/lib/src/editor/find_replace_menu/search_service.dart b/lib/src/editor/find_replace_menu/search_service.dart index 46f116145..9ab8f91aa 100644 --- a/lib/src/editor/find_replace_menu/search_service.dart +++ b/lib/src/editor/find_replace_menu/search_service.dart @@ -1,14 +1,24 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/editor/find_replace_menu/search_algorithm.dart'; +import 'package:flutter/material.dart'; -const foundSelectedColor = '0x6000BCF0'; +class SearchStyle { + SearchStyle({ + this.highlightColor = const Color(0x6000BCF0), + }); + + final Color highlightColor; +} class SearchService { SearchService({ required this.editorState, + required this.style, }); final EditorState editorState; + final SearchStyle style; + //matchedPositions will contain a list of positions of the matched patterns //the position here consists of the node and the starting offset of the //matched pattern. We will use this to traverse between the matched patterns. @@ -25,6 +35,7 @@ class SearchService { //this means we have a new pattern, but before we highlight the new matches, //lets unhiglight the old pattern findAndHighlight(queriedPattern, unhighlight: true); + matchedPositions.clear(); queriedPattern = pattern; } @@ -104,34 +115,40 @@ class SearchService { {AppFlowyRichTextKeys.highlightColor: null}, ); } else { - formatHighlightColor( - editorState, + editorState.formatDelta( selection, - foundSelectedColor, + {AppFlowyRichTextKeys.highlightColor: style.highlightColor.toHex()}, + false, ); } editorState.undoManager.forgetRecentUndo(); } } - void _selectWordAtPosition(Position start, [bool isNavigating = false]) { + Future _selectWordAtPosition( + Position start, [ + bool isNavigating = false, + ]) async { Position end = Position( path: start.path, offset: start.offset + queriedPattern.length, ); - editorState.updateSelectionWithReason( + await editorState.updateSelectionWithReason( Selection(start: start, end: end), reason: isNavigating ? SelectionUpdateReason.searchNavigate : SelectionUpdateReason.searchHighlight, ); + + editorState.service.keyboardService?.disable(); + editorState.service.selectionService.clearCursor(); } /// This method takes in a boolean parameter moveUp, if set to true, /// the match located above the current selected match is newly selected. /// Otherwise the match below the current selected match is newly selected. - void navigateToMatch({bool moveUp = false}) { + void navigateToMatch({bool moveUp = false, bool keepFocus = false}) { if (matchedPositions.isEmpty) return; if (moveUp) { From eaee16150205615ce67636730a25a1b8a4149d91 Mon Sep 17 00:00:00 2001 From: MayurSMahajan Date: Thu, 27 Jul 2023 18:36:46 +0530 Subject: [PATCH 40/45] feat: separate attribute for highlighting --- .../rich_text/appflowy_rich_text.dart | 11 +++++++++++ .../rich_text/appflowy_rich_text_keys.dart | 1 + .../editor/find_replace_menu/search_service.dart | 7 +++++-- .../find_replace_menu_find_test.dart | 11 ++--------- .../find_replace_menu/find_replace_menu_utils.dart | 3 ++- test/new/infra/testable_editor.dart | 13 +++++++++++++ 6 files changed, 34 insertions(+), 12 deletions(-) diff --git a/lib/src/editor/block_component/rich_text/appflowy_rich_text.dart b/lib/src/editor/block_component/rich_text/appflowy_rich_text.dart index 5d38d0757..55935aa69 100644 --- a/lib/src/editor/block_component/rich_text/appflowy_rich_text.dart +++ b/lib/src/editor/block_component/rich_text/appflowy_rich_text.dart @@ -302,6 +302,11 @@ class _AppFlowyRichTextState extends State TextStyle(backgroundColor: attributes.backgroundColor), ); } + if (attributes.findBackgroundColor != null) { + textStyle = textStyle.combine( + TextStyle(backgroundColor: attributes.findBackgroundColor), + ); + } if (attributes.color != null) { textStyle = textStyle.combine( TextStyle(color: attributes.color), @@ -407,6 +412,12 @@ extension AppFlowyRichTextAttributes on Attributes { return highlightColor?.toColor(); } + Color? get findBackgroundColor { + final findBackgroundColor = + this[AppFlowyRichTextKeys.findBackgroundColor] as String?; + return findBackgroundColor?.toColor(); + } + String? get href { if (this[AppFlowyRichTextKeys.href] is String) { return this[AppFlowyRichTextKeys.href]; diff --git a/lib/src/editor/block_component/rich_text/appflowy_rich_text_keys.dart b/lib/src/editor/block_component/rich_text/appflowy_rich_text_keys.dart index 76c9fb2ae..5d0456b43 100644 --- a/lib/src/editor/block_component/rich_text/appflowy_rich_text_keys.dart +++ b/lib/src/editor/block_component/rich_text/appflowy_rich_text_keys.dart @@ -5,6 +5,7 @@ class AppFlowyRichTextKeys { static String strikethrough = 'strikethrough'; static String textColor = 'font_color'; static String highlightColor = 'bg_color'; + static String findBackgroundColor = 'find_bg_color'; static String code = 'code'; static String href = 'href'; diff --git a/lib/src/editor/find_replace_menu/search_service.dart b/lib/src/editor/find_replace_menu/search_service.dart index 9ab8f91aa..614b2ad23 100644 --- a/lib/src/editor/find_replace_menu/search_service.dart +++ b/lib/src/editor/find_replace_menu/search_service.dart @@ -112,12 +112,15 @@ class SearchService { if (unhighlight) { editorState.formatDelta( selection, - {AppFlowyRichTextKeys.highlightColor: null}, + {AppFlowyRichTextKeys.findBackgroundColor: null}, ); } else { editorState.formatDelta( selection, - {AppFlowyRichTextKeys.highlightColor: style.highlightColor.toHex()}, + { + AppFlowyRichTextKeys.findBackgroundColor: + style.highlightColor.toHex() + }, false, ); } diff --git a/test/new/find_replace_menu/find_replace_menu_find_test.dart b/test/new/find_replace_menu/find_replace_menu_find_test.dart index 69184250e..b0b07e3b8 100644 --- a/test/new/find_replace_menu/find_replace_menu_find_test.dart +++ b/test/new/find_replace_menu/find_replace_menu_find_test.dart @@ -210,11 +210,9 @@ void main() async { await tester.pumpAndSettle(); - await enterInputIntoFindDialog(tester, pattern); + expect(find.byType(FindMenuWidget), findsOneWidget); - await editor.pressKey( - key: LogicalKeyboardKey.enter, - ); + await enterInputIntoFindDialog(tester, pattern); //since node at path [1] does not contain match, we expect it //to be not highlighted. @@ -233,11 +231,6 @@ void main() async { pattern = 'Flutter'; await enterInputIntoFindDialog(tester, pattern); - //finds the pattern Flutter - await editor.pressKey( - key: LogicalKeyboardKey.enter, - ); - //we expect that the current selected node is highlighted. checkIfNotHighlighted(node, selectionAtNode1, expectedResult: false); diff --git a/test/new/find_replace_menu/find_replace_menu_utils.dart b/test/new/find_replace_menu/find_replace_menu_utils.dart index 5a14b858c..8cfd3349d 100644 --- a/test/new/find_replace_menu/find_replace_menu_utils.dart +++ b/test/new/find_replace_menu/find_replace_menu_utils.dart @@ -64,7 +64,8 @@ void checkIfNotHighlighted( expect( node.allSatisfyInSelection(selection, (delta) { return delta.whereType().every( - (el) => el.attributes?[AppFlowyRichTextKeys.highlightColor] == null, + (e) => + e.attributes?[AppFlowyRichTextKeys.findBackgroundColor] == null, ); }), expectedResult, diff --git a/test/new/infra/testable_editor.dart b/test/new/infra/testable_editor.dart index db63e65d8..0fb2aea08 100644 --- a/test/new/infra/testable_editor.dart +++ b/test/new/infra/testable_editor.dart @@ -52,6 +52,19 @@ class TestableEditor { autoFocus: autoFocus, shrinkWrap: shrinkWrap, scrollController: scrollController, + commandShortcutEvents: [ + ...standardCommandShortcutEvents, + ...findAndReplaceCommands( + localizations: FindReplaceLocalizations( + find: 'Find', + previousMatch: 'Previous match', + nextMatch: 'Next match', + close: 'Close', + replace: 'Replace', + replaceAll: 'Replace all', + ), + ), + ], editorStyle: inMobile ? const EditorStyle.mobile() : const EditorStyle.desktop(), ); From 335c2d6e20703552b83a63f74545003b64853b84 Mon Sep 17 00:00:00 2001 From: MayurSMahajan Date: Fri, 28 Jul 2023 19:14:33 +0530 Subject: [PATCH 41/45] feat: unique color for selected match --- .../find_replace_command.dart | 8 ++- .../find_replace_widget.dart | 5 +- .../find_replace_menu/search_service.dart | 51 ++++++++++++++----- 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart b/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart index d0631e5a3..25adf188a 100644 --- a/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart +++ b/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart @@ -19,10 +19,14 @@ List findAndReplaceCommands({ class FindReplaceStyle { FindReplaceStyle({ - this.highlightColor = const Color(0x6000BCF0), + this.selectedHighlightColor = const Color(0xFFFFB931), + this.unselectedHighlightColor = const Color(0x60ECBC5F), }); - final Color highlightColor; + //selected highlight color is used as background color on the selected found pattern. + final Color selectedHighlightColor; + //unselected highlight color is used on every other found pattern which can be selected. + final Color unselectedHighlightColor; } class FindReplaceLocalizations { diff --git a/lib/src/editor/find_replace_menu/find_replace_widget.dart b/lib/src/editor/find_replace_menu/find_replace_widget.dart index db4693a25..ed4b21089 100644 --- a/lib/src/editor/find_replace_menu/find_replace_widget.dart +++ b/lib/src/editor/find_replace_menu/find_replace_widget.dart @@ -38,7 +38,10 @@ class _FindMenuWidgetState extends State { replaceFlag = widget.replaceFlag; searchService = SearchService( editorState: widget.editorState, - style: SearchStyle(highlightColor: widget.style.highlightColor), + style: SearchStyle( + selectedHighlightColor: widget.style.selectedHighlightColor, + unselectedHighlightColor: widget.style.unselectedHighlightColor, + ), ); WidgetsBinding.instance.addPostFrameCallback((_) { diff --git a/lib/src/editor/find_replace_menu/search_service.dart b/lib/src/editor/find_replace_menu/search_service.dart index 614b2ad23..bfb7f36e4 100644 --- a/lib/src/editor/find_replace_menu/search_service.dart +++ b/lib/src/editor/find_replace_menu/search_service.dart @@ -4,10 +4,14 @@ import 'package:flutter/material.dart'; class SearchStyle { SearchStyle({ - this.highlightColor = const Color(0x6000BCF0), + this.selectedHighlightColor = const Color(0xFFFFB931), + this.unselectedHighlightColor = const Color(0x60ECBC5F), }); - final Color highlightColor; + //selected highlight color is used as background color on the selected found pattern. + final Color selectedHighlightColor; + //unselected highlight color is used on every other found pattern which can be selected. + final Color unselectedHighlightColor; } class SearchService { @@ -115,14 +119,7 @@ class SearchService { {AppFlowyRichTextKeys.findBackgroundColor: null}, ); } else { - editorState.formatDelta( - selection, - { - AppFlowyRichTextKeys.findBackgroundColor: - style.highlightColor.toHex() - }, - false, - ); + _applySelectedHighlightColor(selection); } editorState.undoManager.forgetRecentUndo(); } @@ -137,8 +134,11 @@ class SearchService { offset: start.offset + queriedPattern.length, ); + final selection = Selection(start: start, end: end); + _applySelectedHighlightColor(selection, isSelected: true); + await editorState.updateSelectionWithReason( - Selection(start: start, end: end), + selection, reason: isNavigating ? SelectionUpdateReason.searchNavigate : SelectionUpdateReason.searchHighlight, @@ -154,6 +154,17 @@ class SearchService { void navigateToMatch({bool moveUp = false, bool keepFocus = false}) { if (matchedPositions.isEmpty) return; + //lets change the highlight color to indicate that the current match is + //not selected. + final currentMatch = matchedPositions[selectedIndex]; + Position end = Position( + path: currentMatch.path, + offset: currentMatch.offset + queriedPattern.length, + ); + + final selection = Selection(start: currentMatch, end: end); + _applySelectedHighlightColor(selection); + if (moveUp) { selectedIndex = selectedIndex - 1 < 0 ? matchedPositions.length - 1 : --selectedIndex; @@ -186,7 +197,7 @@ class SearchService { final selection = editorState.selection!; editorState.formatDelta( selection, - {AppFlowyRichTextKeys.highlightColor: null}, + {AppFlowyRichTextKeys.findBackgroundColor: null}, ); editorState.undoManager.forgetRecentUndo(); @@ -222,4 +233,20 @@ class SearchService { replaceSelectedWord(replaceText); } } + + void _applySelectedHighlightColor( + Selection selection, { + bool isSelected = false, + }) { + final color = isSelected + ? style.selectedHighlightColor.toHex() + : style.unselectedHighlightColor.toHex(); + editorState.formatDelta( + selection, + { + AppFlowyRichTextKeys.findBackgroundColor: color, + }, + false, + ); + } } From 86a8a4a5e07290401d9554444bd4288cbade8593 Mon Sep 17 00:00:00 2001 From: MayurSMahajan Date: Fri, 28 Jul 2023 19:14:52 +0530 Subject: [PATCH 42/45] test: unique color for selected match --- .../find_replace_menu_find_test.dart | 54 ++++++++++++++++++- .../find_replace_menu_utils.dart | 54 +++++++++++++++++++ test/new/infra/testable_editor.dart | 12 +---- 3 files changed, 108 insertions(+), 12 deletions(-) diff --git a/test/new/find_replace_menu/find_replace_menu_find_test.dart b/test/new/find_replace_menu/find_replace_menu_find_test.dart index b0b07e3b8..277ac6b06 100644 --- a/test/new/find_replace_menu/find_replace_menu_find_test.dart +++ b/test/new/find_replace_menu/find_replace_menu_find_test.dart @@ -107,13 +107,13 @@ void main() async { await enterInputIntoFindDialog(tester, pattern); + //this will call naviateToMatch and select the second match await editor.pressKey( key: LogicalKeyboardKey.enter, ); //checking if current selection consists an occurance of matched pattern. - //we expect the last occurance of the pattern to be found, thus that should - //be the current selection. + //we expect the second occurance of the pattern to be found and selected checkCurrentSelection(editor, [1], 0, pattern.length); //now pressing the icon button for previous match should select @@ -147,6 +147,56 @@ void main() async { await editor.dispose(); }); + testWidgets('''navigating - selected match is highlighted uniquely + than unselected matches''', (tester) async { + const pattern = 'Welcome'; + const previousBtnKey = Key('previousMatchButton'); + + final editor = tester.editor; + editor.addParagraphs(3, initialText: text); + + await editor.startTesting(); + + final node0 = editor.nodeAtPath([0]); + final selection0 = getSelectionAtPath([0], 0, pattern.length); + final node1 = editor.nodeAtPath([1]); + final selection1 = getSelectionAtPath([1], 0, pattern.length); + final node2 = editor.nodeAtPath([2]); + final selection2 = getSelectionAtPath([2], 0, pattern.length); + + await editor.updateSelection(Selection.single(path: [0], startOffset: 0)); + + await pressFindAndReplaceCommand(editor); + + await tester.pumpAndSettle(); + + await enterInputIntoFindDialog(tester, pattern); + + //this will call naviateToMatch and select the second match + await editor.pressKey( + key: LogicalKeyboardKey.enter, + ); + + //we expect the second occurance of the pattern to be found and selected + checkCurrentSelection(editor, [1], 0, pattern.length); + + // now lets check if the current selected match is highlighted properly + checkIfHighlightedWithProperColors(node1!, selection1, kSelectedHCHex); + // unselected matches are highlighted with different color + checkIfHighlightedWithProperColors(node2!, selection2, kUnselectedHCHex); + checkIfHighlightedWithProperColors(node0!, selection0, kUnselectedHCHex); + + //press the icon button for previous match should select node at path [0] + await tester.tap(find.byKey(previousBtnKey)); + + checkCurrentSelection(editor, [0], 0, pattern.length); + checkIfHighlightedWithProperColors(node0, selection0, kSelectedHCHex); + checkIfHighlightedWithProperColors(node1, selection1, kUnselectedHCHex); + checkIfHighlightedWithProperColors(node2, selection2, kUnselectedHCHex); + + await editor.dispose(); + }); + testWidgets('found matches are unhighlighted when findMenu closed', (tester) async { const pattern = 'Welcome'; diff --git a/test/new/find_replace_menu/find_replace_menu_utils.dart b/test/new/find_replace_menu/find_replace_menu_utils.dart index 8cfd3349d..e1418d77d 100644 --- a/test/new/find_replace_menu/find_replace_menu_utils.dart +++ b/test/new/find_replace_menu/find_replace_menu_utils.dart @@ -9,6 +9,36 @@ import 'package:flutter_test/flutter_test.dart'; import '../infra/testable_editor.dart'; const text = 'Welcome to Appflowy 😁'; +const Color kSelectedHighlightColor = Colors.yellow; +const Color kUnselectedHighlightColor = Colors.lightGreen; +String kSelectedHCHex = kSelectedHighlightColor.toHex(); +String kUnselectedHCHex = kUnselectedHighlightColor.toHex(); + +class TestableFindAndReplaceCommands { + TestableFindAndReplaceCommands({ + this.selectedHighlightColor = kSelectedHighlightColor, + this.unselectedHighlightColor = kUnselectedHighlightColor, + }); + + final Color selectedHighlightColor; + final Color unselectedHighlightColor; + + List get testableFindAndReplaceCommands => + findAndReplaceCommands( + localizations: FindReplaceLocalizations( + find: 'Find', + previousMatch: 'Previous match', + nextMatch: 'Next match', + close: 'Close', + replace: 'Replace', + replaceAll: 'Replace all', + ), + style: FindReplaceStyle( + selectedHighlightColor: selectedHighlightColor, + unselectedHighlightColor: unselectedHighlightColor, + ), + ); +} Future prepareFindAndReplaceDialog( WidgetTester tester, { @@ -72,6 +102,23 @@ void checkIfNotHighlighted( ); } +void checkIfHighlightedWithProperColors( + Node node, + Selection selection, + String expectedColor, +) { + expect( + node.allSatisfyInSelection(selection, (delta) { + return delta.whereType().every( + (e) => + e.attributes?[AppFlowyRichTextKeys.findBackgroundColor] == + expectedColor, + ); + }), + true, + ); +} + void checkCurrentSelection( TestableEditor editor, Path path, @@ -85,3 +132,10 @@ void checkCurrentSelection( expect(selection!.start, Position(path: path, offset: startOffset)); expect(selection.end, Position(path: path, offset: endOffset)); } + +Selection getSelectionAtPath(Path path, int startOffset, int endOffset) { + return Selection( + start: Position(path: path, offset: startOffset), + end: Position(path: path, offset: endOffset), + ); +} diff --git a/test/new/infra/testable_editor.dart b/test/new/infra/testable_editor.dart index 0fb2aea08..e663986aa 100644 --- a/test/new/infra/testable_editor.dart +++ b/test/new/infra/testable_editor.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; +import '../find_replace_menu/find_replace_menu_utils.dart'; import '../util/util.dart'; class TestableEditor { @@ -54,16 +55,7 @@ class TestableEditor { scrollController: scrollController, commandShortcutEvents: [ ...standardCommandShortcutEvents, - ...findAndReplaceCommands( - localizations: FindReplaceLocalizations( - find: 'Find', - previousMatch: 'Previous match', - nextMatch: 'Next match', - close: 'Close', - replace: 'Replace', - replaceAll: 'Replace all', - ), - ), + ...TestableFindAndReplaceCommands().testableFindAndReplaceCommands, ], editorStyle: inMobile ? const EditorStyle.mobile() : const EditorStyle.desktop(), From 4f1167aecc6eba5c85b289fca74977dc671b106e Mon Sep 17 00:00:00 2001 From: MayurSMahajan Date: Fri, 28 Jul 2023 22:24:02 +0530 Subject: [PATCH 43/45] refactor: cleaning the code --- example/lib/pages/editor.dart | 1 + .../find_replace_command.dart | 48 +++++++------------ .../find_replace_menu/search_service.dart | 13 ++--- .../editor/toolbar/utils/format_color.dart | 2 +- .../find_replace_menu_utils.dart | 3 ++ test/new/infra/testable_editor.dart | 30 +++++++----- 6 files changed, 42 insertions(+), 55 deletions(-) diff --git a/example/lib/pages/editor.dart b/example/lib/pages/editor.dart index bd5779727..1cbfbf0a1 100644 --- a/example/lib/pages/editor.dart +++ b/example/lib/pages/editor.dart @@ -127,6 +127,7 @@ class Editor extends StatelessWidget { commandShortcutEvents: [ ...standardCommandShortcutEvents, ...findAndReplaceCommands( + context: context, localizations: FindReplaceLocalizations( find: 'Find', previousMatch: 'Previous match', diff --git a/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart b/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart index 25adf188a..4882219a0 100644 --- a/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart +++ b/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/find_replace_command.dart @@ -4,15 +4,18 @@ import 'package:flutter/material.dart'; List findAndReplaceCommands({ required FindReplaceLocalizations localizations, + required BuildContext context, FindReplaceStyle? style, }) => [ openFindDialog( localizations: localizations, + context: context, style: style ?? FindReplaceStyle(), ), openReplaceDialog( localizations: localizations, + context: context, style: style ?? FindReplaceStyle(), ), ]; @@ -55,6 +58,7 @@ class FindReplaceLocalizations { /// CommandShortcutEvent openFindDialog({ required FindReplaceLocalizations localizations, + required BuildContext context, required FindReplaceStyle style, }) => CommandShortcutEvent( @@ -62,6 +66,7 @@ CommandShortcutEvent openFindDialog({ command: 'ctrl+f', macOSCommand: 'cmd+f', handler: (editorState) => _showFindAndReplaceDialog( + context, editorState, localizations: localizations, style: style, @@ -70,6 +75,7 @@ CommandShortcutEvent openFindDialog({ CommandShortcutEvent openReplaceDialog({ required FindReplaceLocalizations localizations, + required BuildContext context, required FindReplaceStyle style, }) => CommandShortcutEvent( @@ -77,6 +83,7 @@ CommandShortcutEvent openReplaceDialog({ command: 'ctrl+h', macOSCommand: 'cmd+h', handler: (editorState) => _showFindAndReplaceDialog( + context, editorState, localizations: localizations, style: style, @@ -86,6 +93,7 @@ CommandShortcutEvent openReplaceDialog({ FindReplaceService? _findReplaceService; KeyEventResult _showFindAndReplaceDialog( + BuildContext context, EditorState editorState, { required FindReplaceLocalizations localizations, required FindReplaceStyle style, @@ -95,39 +103,15 @@ KeyEventResult _showFindAndReplaceDialog( return KeyEventResult.ignored; } - final selection = editorState.selection; - if (selection == null) { - return KeyEventResult.ignored; - } - - // // delete the selection - // if (!selection.isCollapsed) { - // await editorState.deleteSelection(selection); - // } - - final afterSelection = editorState.selection; - if (afterSelection == null || !afterSelection.isCollapsed) { - assert(false, 'the selection should be collapsed'); - return KeyEventResult.handled; - } - - // show the slash menu - () { - // this code is copied from the the old editor. - // TODO: refactor this code - final context = editorState.getNodeAtPath(selection.start.path)?.context; - if (context != null) { - _findReplaceService = FindReplaceMenu( - context: context, - editorState: editorState, - replaceFlag: openReplace, - localizations: localizations, - style: style, - ); + _findReplaceService = FindReplaceMenu( + context: context, + editorState: editorState, + replaceFlag: openReplace, + localizations: localizations, + style: style, + ); - _findReplaceService?.show(); - } - }(); + _findReplaceService?.show(); return KeyEventResult.handled; } diff --git a/lib/src/editor/find_replace_menu/search_service.dart b/lib/src/editor/find_replace_menu/search_service.dart index bfb7f36e4..95f01653b 100644 --- a/lib/src/editor/find_replace_menu/search_service.dart +++ b/lib/src/editor/find_replace_menu/search_service.dart @@ -143,15 +143,12 @@ class SearchService { ? SelectionUpdateReason.searchNavigate : SelectionUpdateReason.searchHighlight, ); - - editorState.service.keyboardService?.disable(); - editorState.service.selectionService.clearCursor(); } /// This method takes in a boolean parameter moveUp, if set to true, /// the match located above the current selected match is newly selected. /// Otherwise the match below the current selected match is newly selected. - void navigateToMatch({bool moveUp = false, bool keepFocus = false}) { + void navigateToMatch({bool moveUp = false}) { if (matchedPositions.isEmpty) return; //lets change the highlight color to indicate that the current match is @@ -168,16 +165,12 @@ class SearchService { if (moveUp) { selectedIndex = selectedIndex - 1 < 0 ? matchedPositions.length - 1 : --selectedIndex; - - Position match = matchedPositions[selectedIndex]; - _selectWordAtPosition(match, true); } else { selectedIndex = (selectedIndex + 1) < matchedPositions.length ? ++selectedIndex : 0; - - final match = matchedPositions[selectedIndex]; - _selectWordAtPosition(match, true); } + final match = matchedPositions[selectedIndex]; + _selectWordAtPosition(match, true); } /// Replaces the current selected word with replaceText. diff --git a/lib/src/editor/toolbar/utils/format_color.dart b/lib/src/editor/toolbar/utils/format_color.dart index 2c158d371..7602870c0 100644 --- a/lib/src/editor/toolbar/utils/format_color.dart +++ b/lib/src/editor/toolbar/utils/format_color.dart @@ -18,7 +18,7 @@ void formatFontColor( String color, ) { editorState.formatDelta( - editorState.selection, + selection, {AppFlowyRichTextKeys.textColor: color}, false, ); diff --git a/test/new/find_replace_menu/find_replace_menu_utils.dart b/test/new/find_replace_menu/find_replace_menu_utils.dart index e1418d77d..c11fb4586 100644 --- a/test/new/find_replace_menu/find_replace_menu_utils.dart +++ b/test/new/find_replace_menu/find_replace_menu_utils.dart @@ -16,15 +16,18 @@ String kUnselectedHCHex = kUnselectedHighlightColor.toHex(); class TestableFindAndReplaceCommands { TestableFindAndReplaceCommands({ + required this.context, this.selectedHighlightColor = kSelectedHighlightColor, this.unselectedHighlightColor = kUnselectedHighlightColor, }); final Color selectedHighlightColor; final Color unselectedHighlightColor; + final BuildContext context; List get testableFindAndReplaceCommands => findAndReplaceCommands( + context: context, localizations: FindReplaceLocalizations( find: 'Find', previousMatch: 'Previous match', diff --git a/test/new/infra/testable_editor.dart b/test/new/infra/testable_editor.dart index e663986aa..37ace4e55 100644 --- a/test/new/infra/testable_editor.dart +++ b/test/new/infra/testable_editor.dart @@ -47,18 +47,24 @@ class TestableEditor { if (withFloatingToolbar) { scrollController ??= ScrollController(); } - Widget editor = AppFlowyEditor( - editorState: editorState, - editable: editable, - autoFocus: autoFocus, - shrinkWrap: shrinkWrap, - scrollController: scrollController, - commandShortcutEvents: [ - ...standardCommandShortcutEvents, - ...TestableFindAndReplaceCommands().testableFindAndReplaceCommands, - ], - editorStyle: - inMobile ? const EditorStyle.mobile() : const EditorStyle.desktop(), + Widget editor = Builder( + builder: (context) { + return AppFlowyEditor( + editorState: editorState, + editable: editable, + autoFocus: autoFocus, + shrinkWrap: shrinkWrap, + scrollController: scrollController, + commandShortcutEvents: [ + ...standardCommandShortcutEvents, + ...TestableFindAndReplaceCommands(context: context) + .testableFindAndReplaceCommands, + ], + editorStyle: inMobile + ? const EditorStyle.mobile() + : const EditorStyle.desktop(), + ); + }, ); if (withFloatingToolbar) { if (inMobile) { From f601fe52833dd14fe8d43315266623b3ad88fba6 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Fri, 4 Aug 2023 11:14:58 +0200 Subject: [PATCH 44/45] fix: navigate to first match --- .../find_replace_menu/search_service.dart | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/src/editor/find_replace_menu/search_service.dart b/lib/src/editor/find_replace_menu/search_service.dart index 95f01653b..fdbc0c38b 100644 --- a/lib/src/editor/find_replace_menu/search_service.dart +++ b/lib/src/editor/find_replace_menu/search_service.dart @@ -65,7 +65,7 @@ class SearchService { ); } - selectedIndex = 0; + selectedIndex = -1; } List _getAllTextNodes() { @@ -153,14 +153,16 @@ class SearchService { //lets change the highlight color to indicate that the current match is //not selected. - final currentMatch = matchedPositions[selectedIndex]; - Position end = Position( - path: currentMatch.path, - offset: currentMatch.offset + queriedPattern.length, - ); + if (selectedIndex > -1) { + final currentMatch = matchedPositions[selectedIndex]; + Position end = Position( + path: currentMatch.path, + offset: currentMatch.offset + queriedPattern.length, + ); - final selection = Selection(start: currentMatch, end: end); - _applySelectedHighlightColor(selection); + final selection = Selection(start: currentMatch, end: end); + _applySelectedHighlightColor(selection); + } if (moveUp) { selectedIndex = From 90ba926cde5519afdd413322a0c93394f02ac1f0 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Thu, 10 Aug 2023 12:46:03 +0200 Subject: [PATCH 45/45] fix: tests and replace logic --- .../find_replace_menu/search_service.dart | 45 ++++--- .../find_replace_menu_find_test.dart | 110 +++++++++--------- .../find_replace_menu_replace_test.dart | 28 +++-- 3 files changed, 96 insertions(+), 87 deletions(-) diff --git a/lib/src/editor/find_replace_menu/search_service.dart b/lib/src/editor/find_replace_menu/search_service.dart index fdbc0c38b..916e71e39 100644 --- a/lib/src/editor/find_replace_menu/search_service.dart +++ b/lib/src/editor/find_replace_menu/search_service.dart @@ -178,15 +178,20 @@ class SearchService { /// Replaces the current selected word with replaceText. /// After replacing the selected word, this method selects the next /// matched word if that exists. - void replaceSelectedWord(String replaceText) { + void replaceSelectedWord(String replaceText, [bool fromFirst = false]) { if (replaceText.isEmpty || queriedPattern.isEmpty || matchedPositions.isEmpty) { return; } - final matchedPosition = matchedPositions[selectedIndex]; - _selectWordAtPosition(matchedPosition); + if (selectedIndex == -1) { + selectedIndex++; + } + + final position = + fromFirst ? matchedPositions.first : matchedPositions[selectedIndex]; + _selectWordAtPosition(position); //unhighlight the selected word before it is replaced final selection = editorState.selection!; @@ -196,21 +201,33 @@ class SearchService { ); editorState.undoManager.forgetRecentUndo(); - final textNode = editorState.getNodeAtPath(matchedPosition.path)!; + final textNode = editorState.getNodeAtPath(position.path)!; final transaction = editorState.transaction; transaction.replaceText( textNode, - matchedPosition.offset, + position.offset, queriedPattern.length, replaceText, ); editorState.apply(transaction); - matchedPositions.removeAt(selectedIndex); - navigateToMatch(moveUp: false); + if (fromFirst) { + matchedPositions.removeAt(0); + } else { + matchedPositions.removeAt(selectedIndex); + --selectedIndex; + + if (matchedPositions.isNotEmpty) { + if (selectedIndex == -1) { + selectedIndex = 0; + } + + _selectWordAtPosition(matchedPositions[selectedIndex]); + } + } } /// Replaces all the found occurances of pattern with replaceText @@ -218,14 +235,14 @@ class SearchService { if (replaceText.isEmpty || queriedPattern.isEmpty) { return; } - //we need to create a final variable matchesLength here, because - //when we replaceSelectedWord we reduce the length of matchedPositions - //list, this causes the value to shrink dynamically and thus it may - //result in pretermination. + // We need to create a final variable matchesLength here, because + // when we replaceSelectedWord we reduce the length of matchedPositions + // list, this causes the value to shrink dynamically and thus it may + // result in pretermination. final int matchesLength = matchedPositions.length; for (int i = 0; i < matchesLength; i++) { - replaceSelectedWord(replaceText); + replaceSelectedWord(replaceText, true); } } @@ -238,9 +255,7 @@ class SearchService { : style.unselectedHighlightColor.toHex(); editorState.formatDelta( selection, - { - AppFlowyRichTextKeys.findBackgroundColor: color, - }, + {AppFlowyRichTextKeys.findBackgroundColor: color}, false, ); } diff --git a/test/new/find_replace_menu/find_replace_menu_find_test.dart b/test/new/find_replace_menu/find_replace_menu_find_test.dart index 277ac6b06..e9a8d344e 100644 --- a/test/new/find_replace_menu/find_replace_menu_find_test.dart +++ b/test/new/find_replace_menu/find_replace_menu_find_test.dart @@ -17,9 +17,9 @@ void main() async { testWidgets('appears properly', (tester) async { await prepareFindAndReplaceDialog(tester); - //the prepareFindDialog method only checks if FindMenuWidget is present - //so here we also check if FindMenuWidget contains TextField - //and IconButtons or not. + // The prepareFindDialog method only checks if FindMenuWidget is present + // so here we also check if FindMenuWidget contains TextField + // and IconButtons or not. expect(find.byType(TextField), findsOneWidget); expect(find.byType(IconButton), findsAtLeastNWidgets(4)); @@ -29,7 +29,7 @@ void main() async { testWidgets('disappears when close is called', (tester) async { await prepareFindAndReplaceDialog(tester); - //lets check if find menu disappears if the close button is tapped. + // Check if find menu disappears if the close button is tapped. await tester.tap(find.byKey(const Key('closeButton'))); await tester.pumpAndSettle(); @@ -42,17 +42,17 @@ void main() async { testWidgets('does not highlight anything when empty string searched', (tester) async { - //we expect nothing to be highlighted + // We expect nothing to be highlighted await _prepareFindAndInputPattern(tester, '', true); }); testWidgets('works properly when match is not found', (tester) async { - //we expect nothing to be highlighted + // We expect nothing to be highlighted await _prepareFindAndInputPattern(tester, 'Flutter', true); }); testWidgets('highlights properly when match is found', (tester) async { - //we expect something to be highlighted + // We expect something to be highlighted await _prepareFindAndInputPattern(tester, 'Welcome', false); }); @@ -73,16 +73,16 @@ void main() async { await enterInputIntoFindDialog(tester, pattern); - //checking if current selection consists an occurance of matched pattern. + // Checking if current selection consists an occurance of matched pattern. final selection = editor.editorState.service.selectionService.currentSelection.value; - //we expect the second occurance of the pattern to be found and selected, - //this is because we send a testTextInput.receiveAction(TextInputAction.done) - //event during submitting our text input, thus the second match is selected. + // We expect the first occurance of the pattern to be found and selected, + // this is because we send a testTextInput.receiveAction(TextInputAction.done) + // event during submitting our text input, thus the second match is selected. expect(selection != null, true); - expect(selection!.start, Position(path: [1], offset: 0)); - expect(selection.end, Position(path: [1], offset: pattern.length)); + expect(selection!.start, Position(path: [0], offset: 0)); + expect(selection.end, Position(path: [0], offset: pattern.length)); await editor.dispose(); }); @@ -107,42 +107,36 @@ void main() async { await enterInputIntoFindDialog(tester, pattern); - //this will call naviateToMatch and select the second match + // This will call naviateToMatch and select the first match await editor.pressKey( key: LogicalKeyboardKey.enter, ); - //checking if current selection consists an occurance of matched pattern. - //we expect the second occurance of the pattern to be found and selected - checkCurrentSelection(editor, [1], 0, pattern.length); + // Checking if current selection consists an occurance of matched pattern. + // we expect the first occurance of the pattern to be found and selected + checkCurrentSelection(editor, [0], 0, pattern.length); - //now pressing the icon button for previous match should select - //node at path [0]. + // Now pressing the icon button for previous match should select + // node at path [1]. await tester.tap(find.byKey(previousBtnKey)); await tester.pumpAndSettle(); - checkCurrentSelection(editor, [0], 0, pattern.length); + checkCurrentSelection(editor, [1], 0, pattern.length); - //now pressing the icon button for previous match should select - //node at path [1], since there is no node before node at [0]. await tester.tap(find.byKey(previousBtnKey)); await tester.pumpAndSettle(); - checkCurrentSelection(editor, [1], 0, pattern.length); + checkCurrentSelection(editor, [0], 0, pattern.length); - //now pressing the icon button for next match should select - //node at path[0], since there is no node after node at [1]. await tester.tap(find.byKey(nextBtnKey)); await tester.pumpAndSettle(); - checkCurrentSelection(editor, [0], 0, pattern.length); + checkCurrentSelection(editor, [1], 0, pattern.length); - //now pressing the icon button for next match should select - //node at path [1]. await tester.tap(find.byKey(nextBtnKey)); await tester.pumpAndSettle(); - checkCurrentSelection(editor, [1], 0, pattern.length); + checkCurrentSelection(editor, [0], 0, pattern.length); await editor.dispose(); }); @@ -172,27 +166,28 @@ void main() async { await enterInputIntoFindDialog(tester, pattern); - //this will call naviateToMatch and select the second match + // This will call naviateToMatch and select the first match await editor.pressKey( key: LogicalKeyboardKey.enter, ); - //we expect the second occurance of the pattern to be found and selected - checkCurrentSelection(editor, [1], 0, pattern.length); + // We expect the first occurance of the pattern to be found and selected + checkCurrentSelection(editor, [0], 0, pattern.length); - // now lets check if the current selected match is highlighted properly - checkIfHighlightedWithProperColors(node1!, selection1, kSelectedHCHex); - // unselected matches are highlighted with different color - checkIfHighlightedWithProperColors(node2!, selection2, kUnselectedHCHex); - checkIfHighlightedWithProperColors(node0!, selection0, kUnselectedHCHex); + // Check if the current selected match is highlighted properly + checkIfHighlightedWithProperColors(node0!, selection1, kSelectedHCHex); - //press the icon button for previous match should select node at path [0] + // Unselected matches are highlighted with different color + checkIfHighlightedWithProperColors(node1!, selection2, kUnselectedHCHex); + checkIfHighlightedWithProperColors(node2!, selection0, kUnselectedHCHex); + + // Press the icon button for previous match should select node at path [2] (last match) await tester.tap(find.byKey(previousBtnKey)); - checkCurrentSelection(editor, [0], 0, pattern.length); - checkIfHighlightedWithProperColors(node0, selection0, kSelectedHCHex); - checkIfHighlightedWithProperColors(node1, selection1, kUnselectedHCHex); - checkIfHighlightedWithProperColors(node2, selection2, kUnselectedHCHex); + checkCurrentSelection(editor, [2], 0, pattern.length); + checkIfHighlightedWithProperColors(node2, selection0, kSelectedHCHex); + checkIfHighlightedWithProperColors(node0, selection1, kUnselectedHCHex); + checkIfHighlightedWithProperColors(node1, selection2, kUnselectedHCHex); await editor.dispose(); }); @@ -227,17 +222,17 @@ void main() async { final node = editor.nodeAtPath([2]); expect(node, isNotNull); - //node is highlighted while menu is active + // Node is highlighted while menu is active checkIfNotHighlighted(node!, selection!, expectedResult: false); - //presses the close button + // Presses the close button await tester.tap(find.byKey(closeBtnKey)); await tester.pumpAndSettle(); - //closes the findMenuWidget + // Closes the findMenuWidget expect(find.byType(FindMenuWidget), findsNothing); - //we expect that the current selected node is NOT highlighted. + // We expect that the current selected node is NOT highlighted. checkIfNotHighlighted(node, selection, expectedResult: true); await editor.dispose(); @@ -264,24 +259,24 @@ void main() async { await enterInputIntoFindDialog(tester, pattern); - //since node at path [1] does not contain match, we expect it - //to be not highlighted. + // Since node at path [1] does not contain match, we expect it to not be highlighted. final selectionAtNode1 = Selection.single( path: [1], startOffset: 0, endOffset: textLine2.length, ); - var node = editor.nodeAtPath([1]); + + Node? node = editor.nodeAtPath([1]); expect(node, isNotNull); - //we expect that the current node at path 1 to be NOT highlighted. + // We expect that the current node at path 1 to be NOT highlighted. checkIfNotHighlighted(node!, selectionAtNode1, expectedResult: true); - //now we will change the pattern to Flutter and search it + // Change the pattern to Flutter and search pattern = 'Flutter'; await enterInputIntoFindDialog(tester, pattern); - //we expect that the current selected node is highlighted. + // We expect that the current selected node is highlighted. checkIfNotHighlighted(node, selectionAtNode1, expectedResult: false); final selectionAtNode0 = Selection.single( @@ -292,7 +287,7 @@ void main() async { node = editor.nodeAtPath([0]); expect(node, isNotNull); - //we expect that the current node at path 0 to be NOT highlighted. + // We expect that the current node at path 0 to be NOT highlighted. checkIfNotHighlighted(node!, selectionAtNode0, expectedResult: true); await editor.dispose(); @@ -318,14 +313,15 @@ Future _prepareFindAndInputPattern( expect(find.byType(FindMenuWidget), findsOneWidget); await enterInputIntoFindDialog(tester, pattern); - //pressing enter should trigger the findAndHighlight method, which - //will find the pattern inside the editor. + + // Pressing enter should trigger the findAndHighlight method, which + // will find the pattern inside the editor. await editor.pressKey( key: LogicalKeyboardKey.enter, ); - //since the method will not select anything as searched pattern is - //empty, the current selection should be equal to previous selection. + // Since the method will not select anything as searched pattern is + // empty, the current selection should be equal to previous selection. final selection = Selection.single(path: [0], startOffset: 0, endOffset: text.length); diff --git a/test/new/find_replace_menu/find_replace_menu_replace_test.dart b/test/new/find_replace_menu/find_replace_menu_replace_test.dart index ed2d76f20..8f1bb7bf5 100644 --- a/test/new/find_replace_menu/find_replace_menu_replace_test.dart +++ b/test/new/find_replace_menu/find_replace_menu_replace_test.dart @@ -17,10 +17,10 @@ void main() async { testWidgets('replace menu appears properly', (tester) async { await prepareFindAndReplaceDialog(tester, openReplace: true); - //the prepare method only checks if FindMenuWidget is present - //so here we also check if FindMenuWidget contains TextField - //and IconButtons or not. - //and whether there are two textfields for replace menu as well. + // The prepare method only checks if FindMenuWidget is present + // so here we also check if FindMenuWidget contains TextField + // and IconButtons or not. + // and whether there are two textfields for replace menu as well. expect(find.byType(TextField), findsNWidgets(2)); expect(find.byType(IconButton), findsAtLeastNWidgets(6)); }); @@ -165,17 +165,15 @@ void main() async { await tester.pumpAndSettle(); expect(find.byType(FindMenuWidget), findsOneWidget); - //we put the pattern in the find dialog and press enter + // we put the pattern in the find dialog and press enter await enterInputIntoFindDialog(tester, patternToBeFound); - await editor.pressKey( - key: LogicalKeyboardKey.enter, - ); + await editor.pressKey(key: LogicalKeyboardKey.enter); await tester.pumpAndSettle(); - //lets check after find operation, the second match is selected. - checkCurrentSelection(editor, [1], 0, patternToBeFound.length); + // lets check after find operation, the first match is selected. + checkCurrentSelection(editor, [0], 0, patternToBeFound.length); - //now we input some text into the replace text field and try to replace + // now we input some text into the replace text field and try to replace await enterInputIntoFindDialog( tester, replacePattern, @@ -186,14 +184,14 @@ void main() async { ); await tester.pumpAndSettle(); - //only the node at path 2 should get replaced, all other nodes should stay as before. - final lastNode = editor.nodeAtPath([1]); + //only the node at path 0 should get replaced, all other nodes should stay as before. + final lastNode = editor.nodeAtPath([0]); expect(lastNode!.delta!.toPlainText(), expectedText); - final middleNode = editor.nodeAtPath([2]); + final middleNode = editor.nodeAtPath([1]); expect(middleNode!.delta!.toPlainText(), text); - final firstNode = editor.nodeAtPath([0]); + final firstNode = editor.nodeAtPath([2]); expect(firstNode!.delta!.toPlainText(), text); await editor.dispose();