Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: find dialog 1760 #106

Merged
merged 48 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
e9cc4f3
feat: make find menu widget
MayurSMahajan Apr 10, 2023
99a9a41
feat: service for find menu
MayurSMahajan Apr 10, 2023
ef9eb58
feat: add find menu shortcut event
MayurSMahajan Apr 10, 2023
c95eb99
feat: create a search service
MayurSMahajan Apr 11, 2023
150d49e
docs: explain search service
MayurSMahajan Apr 23, 2023
e29bb40
fix: unhighlight method takes searched word
MayurSMahajan Apr 23, 2023
3d32f34
fix: unhighlight before each search
MayurSMahajan Apr 28, 2023
c79bcec
feat: navigate between matches
MayurSMahajan Apr 28, 2023
2e167a0
feat: forget highlighting from undo stack
MayurSMahajan Apr 28, 2023
ac4b94b
feat: replace logic and ui
MayurSMahajan Apr 30, 2023
fd5c08e
feat: replace shortcut handler and widget
MayurSMahajan May 1, 2023
65221b8
test: find functionality
MayurSMahajan May 1, 2023
0b0ad4d
test: replace menu tests
MayurSMahajan May 1, 2023
6909b32
refactor: separate class for search algo
MayurSMahajan May 11, 2023
329d094
refactor: suggested changes
MayurSMahajan May 11, 2023
8e278a2
refactor: remove unhighlight method
MayurSMahajan May 13, 2023
77986fc
feat: add find highlight color
MayurSMahajan May 21, 2023
2b08ebb
refactor: name of the search algo class
MayurSMahajan May 21, 2023
5417668
test: unit tests for search algorithm
MayurSMahajan May 21, 2023
190cf77
chore: simplify syntax
MayurSMahajan May 21, 2023
7dbb7b5
test: renamed test group
MayurSMahajan May 21, 2023
afd09df
Merge remote-tracking branch 'upstream/main' into fr_find_dialog_1760
MayurSMahajan Jul 13, 2023
47194cf
refactor: move to editor
MayurSMahajan Jul 17, 2023
c54250a
refactor: add shortcut for find
MayurSMahajan Jul 17, 2023
a9c7617
refactor: use new api
MayurSMahajan Jul 18, 2023
5aa776f
refactor: add abstrac class search algo
MayurSMahajan Jul 19, 2023
4a88b42
fix: avoid multiple instances of find dialog
MayurSMahajan Jul 19, 2023
029a6c4
refactor: xazin's suggestions
MayurSMahajan Jul 19, 2023
81ca9a3
chore: separately build input decor
MayurSMahajan Jul 19, 2023
a89f7a9
test: search algorithm
MayurSMahajan Jul 19, 2023
bb0a235
chore: unhighlight properly
MayurSMahajan Jul 19, 2023
a292a05
refactor: replace handler
MayurSMahajan Jul 20, 2023
1d0814f
refactor: move tests into new
MayurSMahajan Jul 20, 2023
afcf621
test: find menu widget test
MayurSMahajan Jul 21, 2023
addeb96
test: replace menu
MayurSMahajan Jul 22, 2023
719006c
fix: localizations + resolve scroll bug partially
Xazin Jul 24, 2023
04946fb
fix: without update selection on highlight
Xazin Jul 24, 2023
8f397bd
fix: do not select words when highlighting
Xazin Jul 25, 2023
b8eaa0a
Merge branch 'main' into fr_find_dialog_1760
Xazin Jul 25, 2023
51d6a0c
test: update expected selection
MayurSMahajan Jul 27, 2023
da3ce2c
fix: matches and styling
Xazin Jul 27, 2023
eaee161
feat: separate attribute for highlighting
MayurSMahajan Jul 27, 2023
335c2d6
feat: unique color for selected match
MayurSMahajan Jul 28, 2023
86a8a4a
test: unique color for selected match
MayurSMahajan Jul 28, 2023
4f1167a
refactor: cleaning the code
MayurSMahajan Jul 28, 2023
f601fe5
fix: navigate to first match
Xazin Aug 4, 2023
2f234b0
Merge branch 'main' into fr_find_dialog_1760
Xazin Aug 10, 2023
90ba926
fix: tests and replace logic
Xazin Aug 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/src/history/undo_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,12 @@ class UndoManager {
),
);
}

void forgetRecentUndo() {
Log.editor.debug('forgetRecentUndo');
if (state == null) {
return;
}
undoStack.pop();
MayurSMahajan marked this conversation as resolved.
Show resolved Hide resolved
}
}
107 changes: 107 additions & 0 deletions lib/src/render/find_replace_menu/find_menu_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
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,
required this.replaceFlag,
});

final BuildContext context;
final EditorState editorState;
final bool replaceFlag;
final double topOffset = 52;
final double rightOffset = 40;

OverlayEntry? _findReplaceMenuEntry;
bool _selectionUpdateByInner = false;

@override
void dismiss() {
if (_findReplaceMenuEntry != null) {
MayurSMahajan marked this conversation as resolved.
Show resolved Hide resolved
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: DecoratedBox(
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,
editorState: editorState,
replaceFlag: replaceFlag,
),
),
),
);
},
);

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();
}
}
152 changes: 152 additions & 0 deletions lib/src/render/find_replace_menu/find_replace_widget.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/render/find_replace_menu/search_service.dart';
import 'package:flutter/material.dart';

MayurSMahajan marked this conversation as resolved.
Show resolved Hide resolved
class FindMenuWidget extends StatefulWidget {
MayurSMahajan marked this conversation as resolved.
Show resolved Hide resolved
const FindMenuWidget({
super.key,
required this.dismiss,
required this.editorState,
required this.replaceFlag,
});

final VoidCallback dismiss;
final EditorState editorState;
final bool replaceFlag;

@override
State<FindMenuWidget> createState() => _FindMenuWidgetState();
}

class _FindMenuWidgetState extends State<FindMenuWidget> {
final findController = TextEditingController();
final replaceController = TextEditingController();
String queriedPattern = '';
bool replaceFlag = false;
late SearchService searchService;

@override
void initState() {
super.initState();
replaceFlag = widget.replaceFlag;
searchService = SearchService(
editorState: widget.editorState,
);
}

@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
children: [
IconButton(
onPressed: () => setState(() {
replaceFlag = !replaceFlag;
}),
icon: replaceFlag
? const Icon(Icons.expand_less)
: const Icon(Icons.expand_more),
),
Padding(
MayurSMahajan marked this conversation as resolved.
Show resolved Hide resolved
padding: const EdgeInsets.all(6.0),
child: SizedBox(
width: 200,
height: 50,
MayurSMahajan marked this conversation as resolved.
Show resolved Hide resolved
child: TextField(
key: const Key('findTextField'),
autofocus: true,
controller: findController,
onSubmitted: (_) => _searchPattern(),
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'Enter text to search',
),
MayurSMahajan marked this conversation as resolved.
Show resolved Hide resolved
),
),
),
IconButton(
key: const Key('previousMatchButton'),
MayurSMahajan marked this conversation as resolved.
Show resolved Hide resolved
onPressed: () => searchService.navigateToMatch(moveUp: true),
icon: const Icon(Icons.arrow_upward),
tooltip: 'Previous Match',
),
IconButton(
key: const Key('nextMatchButton'),
MayurSMahajan marked this conversation as resolved.
Show resolved Hide resolved
onPressed: () => searchService.navigateToMatch(),
icon: const Icon(Icons.arrow_downward),
tooltip: 'Next Match',
),
IconButton(
key: const Key('closeButton'),
MayurSMahajan marked this conversation as resolved.
Show resolved Hide resolved
onPressed: () {
widget.dismiss();
searchService.findAndHighlight(queriedPattern);
setState(() {
queriedPattern = '';
});
},
icon: const Icon(Icons.close),
tooltip: 'Close',
),
],
),
replaceFlag
? Row(
children: [
Padding(
MayurSMahajan marked this conversation as resolved.
Show resolved Hide resolved
padding: const EdgeInsets.all(6.0),
child: SizedBox(
width: 200,
height: 50,
MayurSMahajan marked this conversation as resolved.
Show resolved Hide resolved
child: TextField(
key: const Key('replaceTextField'),
autofocus: false,
controller: replaceController,
onSubmitted: (_) => _replaceSelectedWord(),
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'Replace',
),
),
),
),
IconButton(
onPressed: () => _replaceSelectedWord(),
MayurSMahajan marked this conversation as resolved.
Show resolved Hide resolved
icon: const Icon(Icons.find_replace),
tooltip: 'Replace',
),
IconButton(
key: const Key('replaceAllButton'),
MayurSMahajan marked this conversation as resolved.
Show resolved Hide resolved
onPressed: () => _replaceAllMatches(),
icon: const Icon(Icons.change_circle_outlined),
tooltip: 'Replace All',
),
],
)
: const SizedBox.shrink(),
],
);
}

void _searchPattern() {
searchService.findAndHighlight(findController.text);
setState(() {
queriedPattern = findController.text;
});
}

void _replaceSelectedWord() {
if (findController.text != queriedPattern) {
_searchPattern();
}
searchService.replaceSelectedWord(replaceController.text);
}

void _replaceAllMatches() {
if (findController.text != queriedPattern) {
_searchPattern();
}
searchService.replaceAllMatches(replaceController.text);
}
}
47 changes: 47 additions & 0 deletions lib/src/render/find_replace_menu/search_algorithm.dart
Original file line number Diff line number Diff line change
@@ -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<int> boyerMooreSearch(String pattern, String text) {
int m = pattern.length;
int n = text.length;

Map<String, int> badchar = {};
List<int> 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<String, int> 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;
}
}
}
Loading