feat: add regex and case sensitive to FindReplaceMenu (#480)
This commit is contained in:
parent
897033035c
commit
8bfd779bfe
20 changed files with 723 additions and 41 deletions
51
assets/images/case_sensitive.svg
Normal file
51
assets/images/case_sensitive.svg
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="5.325356mm"
|
||||
height="5.321744mm"
|
||||
viewBox="0 0 5.3253561 5.321744"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="case_sensitive.svg"
|
||||
inkscape:export-filename="case_sensitive.png"
|
||||
inkscape:export-xdpi="95.280991"
|
||||
inkscape:export-ydpi="95.280991"
|
||||
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="18.712277"
|
||||
inkscape:cx="10.207202"
|
||||
inkscape:cy="4.9432787"
|
||||
inkscape:window-width="1440"
|
||||
inkscape:window-height="831"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="图层 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-39.417839,-61.454391)">
|
||||
<path
|
||||
d="m 41.377391,63.050708 q -0.452966,0 -0.706966,0.313266 -0.254,0.309033 -0.254,0.8509 0,0.537633 0.237067,0.846666 0.237066,0.309033 0.719666,0.309033 0.194733,0 0.372533,-0.03387 0.182033,-0.0381 0.368299,-0.09737 v 0.3937 q -0.177799,0.06773 -0.364066,0.09737 -0.182033,0.03387 -0.4318,0.03387 -0.465666,0 -0.770466,-0.190499 -0.304799,-0.194734 -0.457199,-0.541867 -0.1524,-0.351366 -0.1524,-0.821266 0,-0.457199 0.1651,-0.808566 0.169333,-0.351366 0.491066,-0.546099 0.321733,-0.198967 0.787399,-0.198967 0.2286,0 0.4445,0.0508 0.220133,0.04657 0.402166,0.135467 l -0.169333,0.381 q -0.148166,-0.06773 -0.321733,-0.118534 -0.169333,-0.05503 -0.359833,-0.05503 z m 2.256362,2.713564 q -0.474132,0 -0.761999,-0.279399 -0.287866,-0.283633 -0.287866,-0.889 0,-0.423333 0.1397,-0.685799 0.139699,-0.262467 0.385233,-0.385233 0.245533,-0.127 0.558799,-0.127 0.1905,0 0.3556,0.04233 0.169333,0.0381 0.2794,0.0889 l -0.135467,0.364067 q -0.118533,-0.04657 -0.254,-0.08043 -0.131233,-0.03387 -0.249766,-0.03387 -0.618066,0 -0.618066,0.812799 0,0.389466 0.152399,0.592666 0.1524,0.2032 0.448733,0.2032 0.1905,0 0.334433,-0.04233 0.148167,-0.04233 0.270934,-0.110067 v 0.389467 q -0.122767,0.07197 -0.2667,0.105833 -0.143933,0.03387 -0.351367,0.03387 z"
|
||||
id="text1"
|
||||
style="font-weight:500;font-size:4.23333px;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans Medium';stroke-width:1.32292;stroke-linecap:round"
|
||||
aria-label="Cc" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
61
assets/images/regex.svg
Normal file
61
assets/images/regex.svg
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="5.325356mm"
|
||||
height="5.321744mm"
|
||||
viewBox="0 0 5.3253561 5.321744"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="regex.svg"
|
||||
inkscape:export-filename="case_sensitive.png"
|
||||
inkscape:export-xdpi="95.280991"
|
||||
inkscape:export-ydpi="95.280991"
|
||||
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="18.712277"
|
||||
inkscape:cx="10.207202"
|
||||
inkscape:cy="6.4930634"
|
||||
inkscape:window-width="1440"
|
||||
inkscape:window-height="831"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="text1"
|
||||
showgrid="false" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="图层 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-39.417839,-61.454391)">
|
||||
<g
|
||||
id="text1"
|
||||
style="font-weight:500;font-size:4.23333px;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans Medium';stroke-width:1.32292;stroke-linecap:round"
|
||||
aria-label="R*">
|
||||
<path
|
||||
d="m 40.871683,62.61275 q 0.617793,0 0.910907,0.229981 0.297622,0.229981 0.297622,0.698963 0,0.248019 -0.0947,0.419377 -0.09019,0.16685 -0.234492,0.275076 -0.144301,0.103721 -0.302132,0.16685 l 0.901888,1.429491 h -0.559169 l -0.775623,-1.303227 h -0.55917 v 1.303227 h -0.48702 V 62.61275 Z m -0.03156,0.414868 h -0.383302 v 1.095793 h 0.405849 q 0.374284,0 0.545642,-0.139792 0.171359,-0.144303 0.171359,-0.423888 0,-0.288604 -0.180377,-0.410358 -0.180379,-0.121755 -0.559171,-0.121755 z"
|
||||
id="path2"
|
||||
style="stroke-width:1.4092" />
|
||||
<path
|
||||
style="font-weight:bold;font-family:'Droid Sans Fallback';-inkscape-font-specification:'Droid Sans Fallback';stroke-width:1.23353"
|
||||
d="m 44.213106,62.806718 -0.636032,0.333435 0.636032,0.335363 -0.146479,0.27176 -0.641815,-0.354636 v 0.663015 H 43.097158 V 63.39264 l -0.643742,0.354636 -0.146481,-0.27176 0.643743,-0.335363 -0.643743,-0.333435 0.146481,-0.27176 0.643742,0.350781 v -0.663015 h 0.327654 v 0.663015 l 0.641815,-0.350781 z"
|
||||
id="path3"
|
||||
sodipodi:nodetypes="ccccccccccccccccccc" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
|
|
@ -126,6 +126,8 @@
|
|||
"closeFind": "Close",
|
||||
"replace": "Replace",
|
||||
"replaceAll": "Replace all",
|
||||
"regex": "Regex",
|
||||
"caseSensitive": "Case sensitive",
|
||||
"uploadImage": "Upload Image",
|
||||
"urlImage": "URL Image",
|
||||
"incorrectLink": "Incorrect Link",
|
||||
|
|
|
|||
|
|
@ -126,6 +126,8 @@
|
|||
"closeFind": "关闭",
|
||||
"replace": "替换",
|
||||
"replaceAll": "替换全部",
|
||||
"regex": "正则表达式",
|
||||
"caseSensitive": "区分大小写",
|
||||
"uploadImage": "上传图片",
|
||||
"urlImage": "网络图片",
|
||||
"incorrectLink": "链接错误",
|
||||
|
|
|
|||
|
|
@ -126,6 +126,8 @@
|
|||
"closeFind": "關閉",
|
||||
"replace": "取代",
|
||||
"replaceAll": "取代全部",
|
||||
"regex": "正規表示式",
|
||||
"caseSensitive": "區分大小寫",
|
||||
"uploadImage": "上載圖片",
|
||||
"urlImage": "網路圖片",
|
||||
"incorrectLink": "連結錯誤",
|
||||
|
|
|
|||
|
|
@ -122,6 +122,8 @@ KeyEventResult _showFindAndReplaceDialog(
|
|||
showReplaceMenu: openReplace,
|
||||
localizations: localizations,
|
||||
style: style,
|
||||
showRegexButton: true,
|
||||
showCaseSensitiveButton: true,
|
||||
);
|
||||
|
||||
_findReplaceService?.show();
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export 'search_service_v2.dart';
|
||||
export 'search_service_v3.dart' hide MatchWrapper;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ class FindReplaceMenu implements FindReplaceService {
|
|||
required this.showReplaceMenu,
|
||||
this.localizations,
|
||||
required this.style,
|
||||
this.showRegexButton = true,
|
||||
this.showCaseSensitiveButton = true,
|
||||
});
|
||||
|
||||
final BuildContext context;
|
||||
|
|
@ -23,6 +25,8 @@ class FindReplaceMenu implements FindReplaceService {
|
|||
final bool showReplaceMenu;
|
||||
final FindReplaceLocalizations? localizations;
|
||||
final FindReplaceStyle style;
|
||||
final bool showRegexButton;
|
||||
final bool showCaseSensitiveButton;
|
||||
|
||||
final double topOffset = 52;
|
||||
final double rightOffset = 40;
|
||||
|
|
@ -96,6 +100,8 @@ class FindReplaceMenu implements FindReplaceService {
|
|||
showReplaceMenu: showReplaceMenu,
|
||||
localizations: localizations,
|
||||
style: style,
|
||||
showRegexButton: showRegexButton,
|
||||
showCaseSensitiveButton: showCaseSensitiveButton,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/editor/find_replace_menu/find_replace_menu_icon_button.dart';
|
||||
import 'find_replace_menu_icon_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const double _iconButtonSize = 30;
|
||||
|
|
@ -14,6 +14,8 @@ class FindAndReplaceMenuWidget extends StatefulWidget {
|
|||
required this.editorState,
|
||||
required this.showReplaceMenu,
|
||||
required this.style,
|
||||
this.showRegexButton = true,
|
||||
this.showCaseSensitiveButton = true,
|
||||
});
|
||||
|
||||
final EditorState editorState;
|
||||
|
|
@ -26,6 +28,8 @@ class FindAndReplaceMenuWidget extends StatefulWidget {
|
|||
///
|
||||
/// only works for SearchService, not for SearchServiceV2
|
||||
final FindReplaceStyle style;
|
||||
final bool showRegexButton;
|
||||
final bool showCaseSensitiveButton;
|
||||
|
||||
/// The localizations of the find and replace menu
|
||||
final FindReplaceLocalizations? localizations;
|
||||
|
|
@ -42,15 +46,25 @@ class FindAndReplaceMenuWidget extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _FindAndReplaceMenuWidgetState extends State<FindAndReplaceMenuWidget> {
|
||||
final focusNode = FocusNode();
|
||||
final replaceFocusNode = FocusNode();
|
||||
final findController = TextEditingController();
|
||||
final replaceController = TextEditingController();
|
||||
String queriedPattern = '';
|
||||
bool showRegexButton = true;
|
||||
bool showCaseSensitiveButton = true;
|
||||
bool showReplaceMenu = false;
|
||||
|
||||
late SearchServiceV2 searchService = SearchServiceV2(
|
||||
late SearchServiceV3 searchService = SearchServiceV3(
|
||||
editorState: widget.editorState,
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
showReplaceMenu = widget.showReplaceMenu;
|
||||
showRegexButton = widget.showRegexButton;
|
||||
showCaseSensitiveButton = widget.showCaseSensitiveButton;
|
||||
|
||||
showReplaceMenu = widget.showReplaceMenu;
|
||||
}
|
||||
|
|
@ -106,6 +120,8 @@ class FindMenu extends StatefulWidget {
|
|||
required this.style,
|
||||
required this.searchService,
|
||||
required this.onShowReplace,
|
||||
this.showRegexButton = true,
|
||||
this.showCaseSensitiveButton = true,
|
||||
});
|
||||
|
||||
final EditorState editorState;
|
||||
|
|
@ -128,9 +144,12 @@ class FindMenu extends StatefulWidget {
|
|||
/// Whether to show the replace menu or not
|
||||
final bool showReplaceMenu;
|
||||
|
||||
final bool showRegexButton;
|
||||
final bool showCaseSensitiveButton;
|
||||
|
||||
final void Function(bool showReplaceMenu) onShowReplace;
|
||||
|
||||
final SearchServiceV2 searchService;
|
||||
final SearchServiceV3 searchService;
|
||||
|
||||
@override
|
||||
State<FindMenu> createState() => _FindMenuState();
|
||||
|
|
@ -146,6 +165,9 @@ class _FindMenuState extends State<FindMenu> {
|
|||
bool showReplaceMenu = false;
|
||||
bool caseSensitive = false;
|
||||
|
||||
bool showRegexButton = true;
|
||||
bool showCaseSensitiveButton = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -153,7 +175,10 @@ class _FindMenuState extends State<FindMenu> {
|
|||
showReplaceMenu = widget.showReplaceMenu;
|
||||
caseSensitive = widget.caseSensitive;
|
||||
|
||||
widget.searchService.matchedPositions.addListener(_setState);
|
||||
showRegexButton = widget.showRegexButton;
|
||||
showCaseSensitiveButton = widget.showCaseSensitiveButton;
|
||||
|
||||
widget.searchService.matchWrappers.addListener(_setState);
|
||||
widget.searchService.currentSelectedIndex.addListener(_setState);
|
||||
|
||||
findTextEditingController.addListener(_searchPattern);
|
||||
|
|
@ -165,7 +190,7 @@ class _FindMenuState extends State<FindMenu> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.searchService.matchedPositions.removeListener(_setState);
|
||||
widget.searchService.matchWrappers.removeListener(_setState);
|
||||
widget.searchService.currentSelectedIndex.removeListener(_setState);
|
||||
widget.searchService.dispose();
|
||||
findTextEditingController.removeListener(_searchPattern);
|
||||
|
|
@ -178,7 +203,7 @@ class _FindMenuState extends State<FindMenu> {
|
|||
Widget build(BuildContext context) {
|
||||
// the selectedIndex from searchService is 0-based
|
||||
final selectedIndex = widget.searchService.selectedIndex + 1;
|
||||
final matches = widget.searchService.matchedPositions.value;
|
||||
final matches = widget.searchService.matchWrappers.value;
|
||||
return Row(
|
||||
children: [
|
||||
// expand/collapse button
|
||||
|
|
@ -230,21 +255,6 @@ class _FindMenuState extends State<FindMenu> {
|
|||
: '$selectedIndex of ${matches.length}',
|
||||
),
|
||||
),
|
||||
// case sensitive button
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: caseSensitive ? Colors.blue.shade400 : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(0.0),
|
||||
),
|
||||
child: FindAndReplaceMenuIconButton(
|
||||
onPressed: () => setState(() {
|
||||
caseSensitive = !caseSensitive;
|
||||
widget.searchService.caseSensitive = caseSensitive;
|
||||
}),
|
||||
icon: const Icon(Icons.keyboard_capslock),
|
||||
tooltip: 'Aa',
|
||||
),
|
||||
),
|
||||
// previous match button
|
||||
FindAndReplaceMenuIconButton(
|
||||
iconButtonKey: const Key('previousMatchButton'),
|
||||
|
|
@ -268,6 +278,45 @@ class _FindMenuState extends State<FindMenu> {
|
|||
tooltip: widget.localizations?.close ??
|
||||
AppFlowyEditorLocalizations.current.closeFind,
|
||||
),
|
||||
// regex button
|
||||
if (showRegexButton)
|
||||
FindAndReplaceMenuIconButton(
|
||||
key: const Key('findRegexButton'),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
widget.searchService.regex = !widget.searchService.regex;
|
||||
});
|
||||
_searchPattern();
|
||||
},
|
||||
icon: EditorSvg(
|
||||
name: 'regex',
|
||||
width: 20,
|
||||
height: 20,
|
||||
color: widget.searchService.regex ? Colors.black : Colors.grey,
|
||||
),
|
||||
tooltip: AppFlowyEditorLocalizations.current.regex,
|
||||
),
|
||||
// case sensitive button
|
||||
if (showCaseSensitiveButton)
|
||||
FindAndReplaceMenuIconButton(
|
||||
key: const Key('caseSensitiveButton'),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
widget.searchService.caseSensitive =
|
||||
!widget.searchService.caseSensitive;
|
||||
});
|
||||
_searchPattern();
|
||||
},
|
||||
icon: EditorSvg(
|
||||
name: 'case_sensitive',
|
||||
width: 20,
|
||||
height: 20,
|
||||
color: widget.searchService.caseSensitive
|
||||
? Colors.black
|
||||
: Colors.grey,
|
||||
),
|
||||
tooltip: AppFlowyEditorLocalizations.current.caseSensitive,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -298,7 +347,7 @@ class ReplaceMenu extends StatefulWidget {
|
|||
/// The localizations of the find and replace menu
|
||||
final FindReplaceLocalizations? localizations;
|
||||
|
||||
final SearchServiceV2 searchService;
|
||||
final SearchServiceV3 searchService;
|
||||
|
||||
@override
|
||||
State<ReplaceMenu> createState() => _ReplaceMenuState();
|
||||
|
|
|
|||
|
|
@ -5,7 +5,40 @@ import 'dart:math' as math;
|
|||
/// `searchMethod(String pattern, String text)`, here `pattern` is the sequence of
|
||||
/// characters that are to be searched within the `text`.
|
||||
abstract class SearchAlgorithm {
|
||||
List<int> searchMethod(String pattern, String text);
|
||||
Iterable<Match> searchMethod(Pattern pattern, String text);
|
||||
}
|
||||
|
||||
final class BoyerMooreMatch implements Match {
|
||||
const BoyerMooreMatch(
|
||||
this.pattern,
|
||||
this.input,
|
||||
this.start,
|
||||
) : end = start + pattern.length;
|
||||
|
||||
@override
|
||||
final int start;
|
||||
@override
|
||||
final String input;
|
||||
@override
|
||||
final String pattern;
|
||||
@override
|
||||
final int end;
|
||||
@override
|
||||
final int groupCount = 0;
|
||||
|
||||
@override
|
||||
String operator [](int g) => group(g);
|
||||
|
||||
@override
|
||||
String group(int group) {
|
||||
if (group != 0) {
|
||||
throw RangeError.value(group);
|
||||
}
|
||||
return pattern;
|
||||
}
|
||||
|
||||
@override
|
||||
List<String> groups(List<int> groups) => groups.map((e) => group(e)).toList();
|
||||
}
|
||||
|
||||
class BoyerMoore extends SearchAlgorithm {
|
||||
|
|
@ -13,12 +46,20 @@ class BoyerMoore extends SearchAlgorithm {
|
|||
//It is more efficient than brute force searching because it is able to skip
|
||||
//characters that will never possibly match with required pattern.
|
||||
@override
|
||||
List<int> searchMethod(String pattern, String text) {
|
||||
List<Match> searchMethod(Pattern pattern, String text) {
|
||||
if (pattern is String) {
|
||||
return _searchMethod(pattern, text);
|
||||
}
|
||||
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
List<Match> _searchMethod(String pattern, String text) {
|
||||
int m = pattern.length;
|
||||
int n = text.length;
|
||||
|
||||
Map<String, int> badchar = {};
|
||||
List<int> matches = [];
|
||||
List<Match> matches = [];
|
||||
|
||||
_badCharHeuristic(pattern, m, badchar);
|
||||
|
||||
|
|
@ -33,7 +74,7 @@ class BoyerMoore extends SearchAlgorithm {
|
|||
|
||||
//if pattern is present at current shift, the index will become -1
|
||||
if (j < 0) {
|
||||
matches.add(s);
|
||||
matches.add(BoyerMooreMatch(pattern, text, s));
|
||||
s += (s + m < n) ? m - (badchar[text[s + m]] ?? -1) : 1;
|
||||
} else {
|
||||
s += math.max(1, j - (badchar[text[s + j]] ?? -1));
|
||||
|
|
@ -54,3 +95,10 @@ class BoyerMoore extends SearchAlgorithm {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DartBuiltIn extends SearchAlgorithm {
|
||||
@override
|
||||
Iterable<Match> searchMethod(Pattern pattern, String text) {
|
||||
return pattern.allMatches(text);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,8 +49,10 @@ class SearchService {
|
|||
for (final n in _getAllNodes()) {
|
||||
//matches list will contain the offsets where the desired word,
|
||||
//is found.
|
||||
List<int> matches =
|
||||
searchAlgorithm.searchMethod(pattern, n.delta!.toPlainText());
|
||||
List<int> matches = searchAlgorithm
|
||||
.searchMethod(pattern, n.delta!.toPlainText())
|
||||
.map((e) => e.start)
|
||||
.toList();
|
||||
//we will store this list of offsets along with their path,
|
||||
//in a list of positions.
|
||||
for (int matchedOffset in matches) {
|
||||
|
|
|
|||
|
|
@ -78,10 +78,13 @@ class SearchServiceV2 {
|
|||
for (final node in nodes) {
|
||||
if (node.delta != null) {
|
||||
final text = node.delta!.toPlainText();
|
||||
List<int> matches = searchAlgorithm.searchMethod(
|
||||
caseSensitive ? pattern : pattern.toLowerCase(),
|
||||
caseSensitive ? text : text.toLowerCase(),
|
||||
);
|
||||
List<int> matches = searchAlgorithm
|
||||
.searchMethod(
|
||||
caseSensitive ? pattern : pattern.toLowerCase(),
|
||||
caseSensitive ? text : text.toLowerCase(),
|
||||
)
|
||||
.map((e) => e.start)
|
||||
.toList();
|
||||
// we will store this list of offsets along with their path,
|
||||
// in a list of positions.
|
||||
for (int matchedOffset in matches) {
|
||||
|
|
|
|||
259
lib/src/editor/find_replace_menu/search_service_v3.dart
Normal file
259
lib/src/editor/find_replace_menu/search_service_v3.dart
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
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 selectionExtraInfoDisableToolbar = 'selectionExtraInfoDisableToolbar';
|
||||
|
||||
class SearchServiceV3 {
|
||||
SearchServiceV3({
|
||||
required this.editorState,
|
||||
});
|
||||
|
||||
final EditorState editorState;
|
||||
|
||||
//matchedPositions.value 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.
|
||||
ValueNotifier<List<MatchWrapper>> matchWrappers = ValueNotifier([]);
|
||||
SearchAlgorithm searchAlgorithm = DartBuiltIn();
|
||||
String targetString = '';
|
||||
Pattern queriedPattern = RegExp('');
|
||||
|
||||
bool _regex = false;
|
||||
bool get regex => _regex;
|
||||
set regex(bool value) {
|
||||
_regex = value;
|
||||
findAndHighlight(targetString);
|
||||
}
|
||||
|
||||
bool _caseSensitive = false;
|
||||
bool get caseSensitive => _caseSensitive;
|
||||
set caseSensitive(bool value) {
|
||||
_caseSensitive = value;
|
||||
findAndHighlight(targetString);
|
||||
}
|
||||
|
||||
int _selectedIndex = 0;
|
||||
int get selectedIndex => _selectedIndex;
|
||||
set selectedIndex(int index) {
|
||||
_prevSelectedIndex = _selectedIndex;
|
||||
_selectedIndex = matchWrappers.value.isEmpty
|
||||
? -1
|
||||
: index.clamp(0, matchWrappers.value.length - 1);
|
||||
currentSelectedIndex.value = _selectedIndex;
|
||||
}
|
||||
|
||||
// only used for scrolling to the first or the last match.
|
||||
int _prevSelectedIndex = 0;
|
||||
|
||||
ValueNotifier<int> currentSelectedIndex = ValueNotifier(0);
|
||||
|
||||
Pattern _getPattern(String targetString) {
|
||||
if (regex) {
|
||||
return RegExp(targetString, caseSensitive: caseSensitive);
|
||||
} else {
|
||||
return RegExp(RegExp.escape(targetString), caseSensitive: caseSensitive);
|
||||
}
|
||||
}
|
||||
|
||||
String _getRegexReplaced(String replaceText, Match match) {
|
||||
List<String?> groups = match
|
||||
.groups(List<int>.generate(match.groupCount + 1, (index) => index));
|
||||
|
||||
String replacedText = replaceText;
|
||||
for (int i = 0; i <= match.groupCount; i++) {
|
||||
replacedText = replacedText.replaceAll('\\$i', groups[i] ?? '');
|
||||
}
|
||||
|
||||
return replacedText;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
matchWrappers.dispose();
|
||||
currentSelectedIndex.dispose();
|
||||
}
|
||||
|
||||
// Public entry method for _findAndHighlight, do necessary checks
|
||||
// and clear previous highlights before calling the private method
|
||||
void findAndHighlight(String target, {bool unHighlight = false}) {
|
||||
Pattern pattern = _getPattern(target);
|
||||
|
||||
if (queriedPattern != pattern) {
|
||||
// this means we have a new pattern, but before we highlight the new matches,
|
||||
// lets unhighlight the old pattern
|
||||
_findAndHighlight(queriedPattern, unHighlight: true);
|
||||
matchWrappers.value.clear();
|
||||
queriedPattern = pattern;
|
||||
targetString = target;
|
||||
}
|
||||
|
||||
if (target.isEmpty) return;
|
||||
|
||||
_findAndHighlight(pattern, unHighlight: unHighlight);
|
||||
}
|
||||
|
||||
/// 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(
|
||||
Pattern pattern, {
|
||||
bool unHighlight = false,
|
||||
}) {
|
||||
matchWrappers.value = _getMatchWrappers(
|
||||
pattern: pattern,
|
||||
nodes: editorState.document.root.children,
|
||||
);
|
||||
|
||||
if (matchWrappers.value.isNotEmpty) {
|
||||
selectedIndex = selectedIndex;
|
||||
_highlightCurrentMatch(
|
||||
pattern,
|
||||
);
|
||||
} else {
|
||||
editorState.updateSelectionWithReason(null);
|
||||
}
|
||||
}
|
||||
|
||||
List<MatchWrapper> _getMatchWrappers({
|
||||
required Pattern pattern,
|
||||
Iterable<Node> nodes = const [],
|
||||
}) {
|
||||
final List<MatchWrapper> result = [];
|
||||
for (final node in nodes) {
|
||||
if (node.delta != null) {
|
||||
final text = node.delta!.toPlainText();
|
||||
final matches = searchAlgorithm.searchMethod(pattern, text);
|
||||
// we will store this list of offsets along with their path,
|
||||
// in a list of positions.
|
||||
for (Match match in matches) {
|
||||
result.add(
|
||||
MatchWrapper(match, node.path),
|
||||
);
|
||||
}
|
||||
}
|
||||
result.addAll(
|
||||
_getMatchWrappers(pattern: pattern, nodes: node.children),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void _highlightCurrentMatch(
|
||||
Pattern pattern,
|
||||
) {
|
||||
final selection = matchWrappers.value[selectedIndex].selection;
|
||||
|
||||
// https://github.com/google/flutter.widgets/issues/151
|
||||
// there's a bug in the scrollable_positioned_list package
|
||||
// we can't scroll to the index without animation.
|
||||
// so we just scroll the position if the index is the first or the last.
|
||||
final length = matchWrappers.value.length - 1;
|
||||
if (_prevSelectedIndex != selectedIndex &&
|
||||
((_prevSelectedIndex == length && selectedIndex == 0) ||
|
||||
(_prevSelectedIndex == 0 && selectedIndex == length))) {
|
||||
editorState.scrollService?.jumpTo(selection.start.path.first);
|
||||
}
|
||||
|
||||
editorState.updateSelectionWithReason(
|
||||
selection,
|
||||
extraInfo: {
|
||||
selectionExtraInfoDisableToolbar: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 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}) {
|
||||
if (matchWrappers.value.isEmpty) return;
|
||||
|
||||
if (moveUp) {
|
||||
selectedIndex = selectedIndex <= 0
|
||||
? matchWrappers.value.length - 1
|
||||
: selectedIndex - 1;
|
||||
} else {
|
||||
selectedIndex = selectedIndex >= matchWrappers.value.length - 1
|
||||
? 0
|
||||
: selectedIndex + 1;
|
||||
}
|
||||
|
||||
_highlightCurrentMatch(queriedPattern);
|
||||
}
|
||||
|
||||
/// Replaces the current selected word with replaceText.
|
||||
/// After replacing the selected word, this method selects the next
|
||||
/// matched word if that exists.
|
||||
Future<void> replaceSelectedWord(String replaceText) async {
|
||||
if (replaceText.isEmpty ||
|
||||
queriedPattern.toString().isEmpty ||
|
||||
matchWrappers.value.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final matchWrap = matchWrappers.value[selectedIndex];
|
||||
final node = editorState.getNodeAtPath(matchWrap.path)!;
|
||||
|
||||
final String replaced;
|
||||
if (regex) {
|
||||
replaced = _getRegexReplaced(replaceText, matchWrap.match);
|
||||
} else {
|
||||
replaced = replaceText;
|
||||
}
|
||||
|
||||
final transaction = editorState.transaction
|
||||
..replaceText(
|
||||
node,
|
||||
matchWrap.selection.start.offset,
|
||||
matchWrap.selection.length,
|
||||
replaced,
|
||||
);
|
||||
await editorState.apply(transaction);
|
||||
|
||||
matchWrappers.value.clear();
|
||||
_findAndHighlight(queriedPattern);
|
||||
}
|
||||
|
||||
/// Replaces all the found occurrences of pattern with replaceText
|
||||
void replaceAllMatches(String replaceText) {
|
||||
if (replaceText.isEmpty ||
|
||||
queriedPattern.toString().isEmpty ||
|
||||
matchWrappers.value.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
// _highlightAllMatches(queriedPattern.length, unHighlight: true);
|
||||
for (final matchWrap in matchWrappers.value.reversed) {
|
||||
final node = editorState.getNodeAtPath(matchWrap.path)!;
|
||||
final String replaced;
|
||||
if (regex) {
|
||||
replaced = _getRegexReplaced(replaceText, matchWrap.match);
|
||||
} else {
|
||||
replaced = replaceText;
|
||||
}
|
||||
|
||||
final transaction = editorState.transaction
|
||||
..replaceText(
|
||||
node,
|
||||
matchWrap.selection.startIndex,
|
||||
matchWrap.selection.length,
|
||||
replaced,
|
||||
);
|
||||
|
||||
editorState.apply(transaction);
|
||||
}
|
||||
matchWrappers.value.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class MatchWrapper {
|
||||
MatchWrapper(this.match, this.path);
|
||||
|
||||
final Match match;
|
||||
final Path path;
|
||||
|
||||
Selection get selection => Selection(
|
||||
start: Position(path: path, offset: match.start),
|
||||
end: Position(path: path, offset: match.end),
|
||||
);
|
||||
}
|
||||
|
|
@ -49,6 +49,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"bold": MessageLookupByLibrary.simpleMessage("Bold"),
|
||||
"bulletedList": MessageLookupByLibrary.simpleMessage("Bulleted List"),
|
||||
"cancel": MessageLookupByLibrary.simpleMessage("Cancel"),
|
||||
"caseSensitive": MessageLookupByLibrary.simpleMessage("Case sensitive"),
|
||||
"checkbox": MessageLookupByLibrary.simpleMessage("Checkbox"),
|
||||
"chooseImage": MessageLookupByLibrary.simpleMessage("Choose an image"),
|
||||
"clearHighlightColor":
|
||||
|
|
@ -118,6 +119,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"paste": MessageLookupByLibrary.simpleMessage("Paste"),
|
||||
"previousMatch": MessageLookupByLibrary.simpleMessage("Previous match"),
|
||||
"quote": MessageLookupByLibrary.simpleMessage("Quote"),
|
||||
"regex": MessageLookupByLibrary.simpleMessage("Regex"),
|
||||
"removeLink": MessageLookupByLibrary.simpleMessage("Remove link"),
|
||||
"replace": MessageLookupByLibrary.simpleMessage("Replace"),
|
||||
"replaceAll": MessageLookupByLibrary.simpleMessage("Replace all"),
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"bold": MessageLookupByLibrary.simpleMessage("粗体"),
|
||||
"bulletedList": MessageLookupByLibrary.simpleMessage("无序列表"),
|
||||
"cancel": MessageLookupByLibrary.simpleMessage("取消"),
|
||||
"caseSensitive": MessageLookupByLibrary.simpleMessage("区分大小写"),
|
||||
"checkbox": MessageLookupByLibrary.simpleMessage("复选框"),
|
||||
"chooseImage": MessageLookupByLibrary.simpleMessage("选择图片文件"),
|
||||
"clearHighlightColor": MessageLookupByLibrary.simpleMessage("清除高亮颜色"),
|
||||
|
|
@ -102,6 +103,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"paste": MessageLookupByLibrary.simpleMessage("粘贴"),
|
||||
"previousMatch": MessageLookupByLibrary.simpleMessage("上一匹配项"),
|
||||
"quote": MessageLookupByLibrary.simpleMessage("引文"),
|
||||
"regex": MessageLookupByLibrary.simpleMessage("正则表达式"),
|
||||
"removeLink": MessageLookupByLibrary.simpleMessage("移除链接"),
|
||||
"replace": MessageLookupByLibrary.simpleMessage("替换"),
|
||||
"replaceAll": MessageLookupByLibrary.simpleMessage("替换全部"),
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"bold": MessageLookupByLibrary.simpleMessage("粗體"),
|
||||
"bulletedList": MessageLookupByLibrary.simpleMessage("無序列表"),
|
||||
"cancel": MessageLookupByLibrary.simpleMessage("取消"),
|
||||
"caseSensitive": MessageLookupByLibrary.simpleMessage("區分大小寫"),
|
||||
"checkbox": MessageLookupByLibrary.simpleMessage("核取方塊"),
|
||||
"chooseImage": MessageLookupByLibrary.simpleMessage("選取圖像檔案"),
|
||||
"clearHighlightColor": MessageLookupByLibrary.simpleMessage("清除高亮顏色"),
|
||||
|
|
@ -102,6 +103,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"paste": MessageLookupByLibrary.simpleMessage("貼上"),
|
||||
"previousMatch": MessageLookupByLibrary.simpleMessage("上一相符項"),
|
||||
"quote": MessageLookupByLibrary.simpleMessage("引文"),
|
||||
"regex": MessageLookupByLibrary.simpleMessage("正規表示式"),
|
||||
"removeLink": MessageLookupByLibrary.simpleMessage("移除連結"),
|
||||
"replace": MessageLookupByLibrary.simpleMessage("取代"),
|
||||
"replaceAll": MessageLookupByLibrary.simpleMessage("取代全部"),
|
||||
|
|
|
|||
|
|
@ -941,6 +941,26 @@ class AppFlowyEditorLocalizations {
|
|||
);
|
||||
}
|
||||
|
||||
/// `Regex`
|
||||
String get regex {
|
||||
return Intl.message(
|
||||
'Regex',
|
||||
name: 'regex',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Case sensitive`
|
||||
String get caseSensitive {
|
||||
return Intl.message(
|
||||
'Case sensitive',
|
||||
name: 'caseSensitive',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Upload Image`
|
||||
String get uploadImage {
|
||||
return Intl.message(
|
||||
|
|
|
|||
|
|
@ -306,5 +306,147 @@ void main() async {
|
|||
expect(node!.delta!.toPlainText(), replacePattern * 5);
|
||||
await editor.dispose();
|
||||
});
|
||||
|
||||
testWidgets('replace all regex matches', (tester) async {
|
||||
const patternToBeFound = 'a[a-z]p';
|
||||
const replacePattern = 'axp';
|
||||
const expectedText =
|
||||
'Welcome to the Appflowy exaxple axp, an axpha-level editor for caxpuses 😁';
|
||||
const replaceAllBtn = Key('replaceAllButton');
|
||||
const regexButton = Key('findRegexButton');
|
||||
const caseSensitiveButton = Key('caseSensitiveButton');
|
||||
|
||||
final editor = tester.editor;
|
||||
editor.addParagraphs(1, initialText: regexTarget);
|
||||
|
||||
await editor.startTesting();
|
||||
await editor.updateSelection(Selection.single(path: [0], startOffset: 0));
|
||||
|
||||
await pressFindAndReplaceCommand(editor, openReplace: true);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(FindAndReplaceMenuWidget), 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();
|
||||
|
||||
await tester.tap(find.byKey(regexButton));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(caseSensitiveButton));
|
||||
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
|
||||
final node = editor.nodeAtPath([0]);
|
||||
expect(node!.delta!.toPlainText(), expectedText);
|
||||
await editor.dispose();
|
||||
});
|
||||
|
||||
testWidgets('replace all regex matches with case insensitive',
|
||||
(tester) async {
|
||||
const patternToBeFound = 'a[a-z]p';
|
||||
const replacePattern = 'axp';
|
||||
const expectedText =
|
||||
'Welcome to the axpflowy exaxple axp, an axpha-level editor for caxpuses 😁';
|
||||
const replaceAllBtn = Key('replaceAllButton');
|
||||
const regexButton = Key('findRegexButton');
|
||||
|
||||
final editor = tester.editor;
|
||||
editor.addParagraphs(1, initialText: regexTarget);
|
||||
|
||||
await editor.startTesting();
|
||||
await editor.updateSelection(Selection.single(path: [0], startOffset: 0));
|
||||
|
||||
await pressFindAndReplaceCommand(editor, openReplace: true);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(FindAndReplaceMenuWidget), 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();
|
||||
|
||||
await tester.tap(find.byKey(regexButton));
|
||||
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
|
||||
final node = editor.nodeAtPath([0]);
|
||||
expect(node!.delta!.toPlainText(), expectedText);
|
||||
await editor.dispose();
|
||||
});
|
||||
|
||||
testWidgets('replace all regex matches with backreference', (tester) async {
|
||||
const patternToBeFound = 'a([a-z])p';
|
||||
const replacePattern = r'b\1q';
|
||||
const expectedText =
|
||||
'Welcome to the Appflowy exbmqle bpq, an blqha-level editor for cbmquses 😁';
|
||||
const replaceAllBtn = Key('replaceAllButton');
|
||||
const regexButton = Key('findRegexButton');
|
||||
const caseSensitiveButton = Key('caseSensitiveButton');
|
||||
|
||||
final editor = tester.editor;
|
||||
editor.addParagraphs(1, initialText: regexTarget);
|
||||
|
||||
await editor.startTesting();
|
||||
await editor.updateSelection(Selection.single(path: [0], startOffset: 0));
|
||||
|
||||
await pressFindAndReplaceCommand(editor, openReplace: true);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(FindAndReplaceMenuWidget), 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();
|
||||
|
||||
await tester.tap(find.byKey(regexButton));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(caseSensitiveButton));
|
||||
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
|
||||
final node = editor.nodeAtPath([0]);
|
||||
expect(node!.delta!.toPlainText(), expectedText);
|
||||
await editor.dispose();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import 'package:flutter_test/flutter_test.dart';
|
|||
import '../infra/testable_editor.dart';
|
||||
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
const regexTarget =
|
||||
'Welcome to the Appflowy example app, an alpha-level editor for campuses 😁';
|
||||
const Color kSelectedHighlightColor = Colors.yellow;
|
||||
const Color kUnselectedHighlightColor = Colors.lightGreen;
|
||||
String kSelectedHCHex = kSelectedHighlightColor.toHex();
|
||||
|
|
|
|||
|
|
@ -3,17 +3,18 @@ import 'package:appflowy_editor/src/editor/find_replace_menu/search_algorithm.da
|
|||
|
||||
void main() {
|
||||
group('search_algorithm_test.dart', () {
|
||||
late SearchAlgorithm searchAlgorithm;
|
||||
late SearchAlgorithm algorithm;
|
||||
|
||||
setUp(() {
|
||||
searchAlgorithm = BoyerMoore();
|
||||
algorithm = DartBuiltIn();
|
||||
});
|
||||
|
||||
test('search algorithm returns the index of the only found pattern', () {
|
||||
const pattern = 'Appflowy';
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
|
||||
List<int> result = searchAlgorithm.searchMethod(pattern, text);
|
||||
List<int> result =
|
||||
algorithm.searchMethod(pattern, text).map((e) => e.start).toList();
|
||||
expect(result, [11]);
|
||||
});
|
||||
|
||||
|
|
@ -29,7 +30,8 @@ grouped by property. Design and modify Appflowy your way with an
|
|||
open core codebase. Appflowy is built with Flutter and Rust.
|
||||
''';
|
||||
|
||||
List<int> result = searchAlgorithm.searchMethod(pattern, text);
|
||||
List<int> result =
|
||||
algorithm.searchMethod(pattern, text).map((e) => e.start).toList();
|
||||
expect(result, [11, 24, 80, 196, 324, 371]);
|
||||
});
|
||||
|
||||
|
|
@ -37,7 +39,7 @@ open core codebase. Appflowy is built with Flutter and Rust.
|
|||
const pattern = 'Flutter';
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
|
||||
final result = searchAlgorithm.searchMethod(pattern, text);
|
||||
final result = algorithm.searchMethod(pattern, text);
|
||||
|
||||
expect(result, []);
|
||||
});
|
||||
|
|
@ -46,7 +48,8 @@ open core codebase. Appflowy is built with Flutter and Rust.
|
|||
const pattern = '😁';
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
|
||||
List<int> result = searchAlgorithm.searchMethod(pattern, text);
|
||||
List<int> result =
|
||||
algorithm.searchMethod(pattern, text).map((e) => e.start).toList();
|
||||
expect(result, [20]);
|
||||
});
|
||||
|
||||
|
|
@ -56,7 +59,8 @@ open core codebase. Appflowy is built with Flutter and Rust.
|
|||
const pattern = 'App';
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
|
||||
List<int> result = searchAlgorithm.searchMethod(pattern, text);
|
||||
List<int> result =
|
||||
algorithm.searchMethod(pattern, text).map((e) => e.start).toList();
|
||||
expect(result, [11]);
|
||||
});
|
||||
|
||||
|
|
@ -64,8 +68,29 @@ open core codebase. Appflowy is built with Flutter and Rust.
|
|||
const pattern = 'APPFLOWY';
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
|
||||
List<int> result = searchAlgorithm.searchMethod(pattern, text);
|
||||
List<int> result =
|
||||
algorithm.searchMethod(pattern, text).map((e) => e.start).toList();
|
||||
expect(result, []);
|
||||
});
|
||||
|
||||
test('case insensitive search', () async {
|
||||
final pattern = RegExp('APPFLOWY', caseSensitive: false);
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
|
||||
List<int> result =
|
||||
algorithm.searchMethod(pattern, text).map((e) => e.start).toList();
|
||||
expect(result, [11]);
|
||||
});
|
||||
|
||||
test('regex search', () async {
|
||||
final pattern = RegExp('a[a-z]p', caseSensitive: false);
|
||||
const text = 'Welcome to Appflowy example app 😁';
|
||||
|
||||
Iterable<Match> result = algorithm.searchMethod(pattern, text);
|
||||
final starts = result.map((e) => e.start).toList();
|
||||
final ends = result.map((e) => e.end).toList();
|
||||
expect(starts, [11, 22, 28]);
|
||||
expect(ends, [14, 25, 31]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue