Skip to content

Commit

Permalink
style: merge branch 'main' into feat/table-plugin
Browse files Browse the repository at this point in the history
* main:
  chore: comment suggesting use of deprecated method (AppFlowy-IO#417)
  fix: replace matches on the same node (AppFlowy-IO#418)
  • Loading branch information
zoli committed Aug 28, 2023
2 parents 2032f77 + c722ec1 commit ce0dd85
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 58 deletions.
2 changes: 0 additions & 2 deletions lib/src/core/location/selection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ class Selection {
}) : start = Position(path: path, offset: startOffset),
end = Position(path: path, offset: endOffset ?? startOffset);

/// deprecated: use [Selection.collapse] instead.
/// Create a collapsed selection with [position].
///
Selection.collapsed(Position position)
: start = position,
end = position;
Expand Down
1 change: 1 addition & 0 deletions lib/src/editor/find_replace_menu/find_replace_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ class _FindMenuWidgetState extends State<FindMenuWidget> {
),
),
IconButton(
key: const Key('replaceSelectedButton'),
onPressed: () => _replaceSelectedWord(),
icon: const Icon(Icons.find_replace),
iconSize: _iconSize,
Expand Down
94 changes: 38 additions & 56 deletions lib/src/editor/find_replace_menu/search_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class SearchService {
if (pattern.isEmpty) return;

//traversing all the nodes
for (final n in _getAllTextNodes()) {
for (final n in _getAllNodes()) {
//matches list will contain the offsets where the desired word,
//is found.
List<int> matches =
Expand All @@ -56,19 +56,14 @@ class SearchService {
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,
);
}
//finally we will highlight all the mathces.
_highlightAllMatches(pattern.length, unhighlight: unhighlight);

selectedIndex = -1;
}

List<Node> _getAllTextNodes() {
List<Node> _getAllNodes() {
final contents = editorState.document.root.children;

if (contents.isEmpty) return [];
Expand All @@ -93,22 +88,15 @@ class SearchService {
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<int> matches,
void _highlightAllMatches(
int patternLength, {
bool unhighlight = false,
}) {
for (final match in matches) {
final start = Position(path: path, offset: match);
for (final match in matchedPositions) {
final start = Position(path: match.path, offset: match.offset);
final end = Position(
path: start.path,
offset: start.offset + queriedPattern.length,
path: match.path,
offset: match.offset + patternLength,
);

final selection = Selection(start: start, end: end);
Expand Down Expand Up @@ -178,7 +166,7 @@ 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, [bool fromFirst = false]) {
void replaceSelectedWord(String replaceText) {
if (replaceText.isEmpty ||
queriedPattern.isEmpty ||
matchedPositions.isEmpty) {
Expand All @@ -189,8 +177,7 @@ class SearchService {
selectedIndex++;
}

final position =
fromFirst ? matchedPositions.first : matchedPositions[selectedIndex];
final position = matchedPositions[selectedIndex];
_selectWordAtPosition(position);

//unhighlight the selected word before it is replaced
Expand All @@ -202,48 +189,43 @@ class SearchService {
editorState.undoManager.forgetRecentUndo();

final textNode = editorState.getNodeAtPath(position.path)!;

final transaction = editorState.transaction;

transaction.replaceText(
textNode,
position.offset,
queriedPattern.length,
replaceText,
);
final transaction = editorState.transaction
..replaceText(
textNode,
position.offset,
queriedPattern.length,
replaceText,
);

editorState.apply(transaction);

if (fromFirst) {
matchedPositions.removeAt(0);
} else {
matchedPositions.removeAt(selectedIndex);
--selectedIndex;

if (matchedPositions.isNotEmpty) {
if (selectedIndex == -1) {
selectedIndex = 0;
}

_selectWordAtPosition(matchedPositions[selectedIndex]);
}
}
matchedPositions.clear();
findAndHighlight(queriedPattern);
}

/// Replaces all the found occurances of pattern with replaceText
void replaceAllMatches(String replaceText) {
if (replaceText.isEmpty || queriedPattern.isEmpty) {
if (replaceText.isEmpty ||
queriedPattern.isEmpty ||
matchedPositions.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, true);

_highlightAllMatches(queriedPattern.length, unhighlight: true);
for (final match in matchedPositions.reversed.toList()) {
final node = editorState.getNodeAtPath(match.path)!;

final transaction = editorState.transaction
..replaceText(
node,
match.offset,
queriedPattern.length,
replaceText,
);

editorState.apply(transaction);
}
matchedPositions.clear();
}

void _applySelectedHighlightColor(
Expand Down
72 changes: 72 additions & 0 deletions test/new/find_replace_menu/find_replace_menu_replace_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,44 @@ void main() async {
await editor.dispose();
});

testWidgets('replace match on multiple matches in same path',
(tester) async {
const patternToBeFound = 'a';
const replacePattern = 'test';
const replaceSelectedBtn = Key('replaceSelectedButton');
const multiplier = 5;

final editor = tester.editor;
editor.addParagraph(initialText: patternToBeFound * multiplier);

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);

//now we input some text into the replace text field and try replace all
await enterInputIntoReplaceDialog(
tester,
replacePattern,
);

for (int i = 0; i < multiplier; i++) {
await tester.tap(find.byKey(replaceSelectedBtn));
await tester.pumpAndSettle();
}

//all matches should be replaced
final node = editor.nodeAtPath([0]);
expect(node!.delta!.toPlainText(), replacePattern * multiplier);
await editor.dispose();
});

testWidgets('replace all on found matches', (tester) async {
const patternToBeFound = 'Welcome';
const replacePattern = 'Salute';
Expand Down Expand Up @@ -239,5 +277,39 @@ void main() async {
}
await editor.dispose();
});

testWidgets('replace all on found matches in same path', (tester) async {
const patternToBeFound = 'x';
const replacePattern = 'Mayur';
const replaceAllBtn = Key('replaceAllButton');

final editor = tester.editor;
editor.addParagraph(initialText: patternToBeFound * 5);

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);

//now we input some text into the replace text field and try replace all
await enterInputIntoReplaceDialog(
tester,
replacePattern,
);

await tester.tap(find.byKey(replaceAllBtn));
await tester.pumpAndSettle();

//all matches should be replaced
final node = editor.nodeAtPath([0]);
expect(node!.delta!.toPlainText(), replacePattern * 5);
await editor.dispose();
});
});
}
10 changes: 10 additions & 0 deletions test/new/find_replace_menu/find_replace_menu_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ Future<void> enterInputIntoFindDialog(
await tester.pumpAndSettle();
}

Future<void> enterInputIntoReplaceDialog(
WidgetTester tester,
String pattern,
) async {
const textInputKey = Key('replaceTextField');
await tester.tap(find.byKey(textInputKey));
await tester.enterText(find.byKey(textInputKey), pattern);
await tester.pumpAndSettle();
}

Future<void> pressFindAndReplaceCommand(
TestableEditor editor, {
bool openReplace = false,
Expand Down

0 comments on commit ce0dd85

Please sign in to comment.