fix: replace matches on the same node (#418)

* test: failing test for expected behavior

* test: fix test by removing enter

* fix: replace all fixed

* chore: remove unused code

* test: replace selected called multiple times

* chore: remove unused variable
This commit is contained in:
Mayur Mahajan 2023-08-28 08:32:41 +05:30 • committed by GitHub
commit e38b07d13b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 121 additions and 56 deletions

View file

@ -129,6 +129,7 @@ class _FindMenuWidgetState extends State<FindMenuWidget> {
),
),
IconButton(
key: const Key('replaceSelectedButton'),
onPressed: () => _replaceSelectedWord(),
icon: const Icon(Icons.find_replace),
iconSize: _iconSize,

View file

@ -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 =
@ -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 [];
@ -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);
@ -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) {
@ -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
@ -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(

View file

@ -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';
@ -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();
});
});
}

View file

@ -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,