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( IconButton(
key: const Key('replaceSelectedButton'),
onPressed: () => _replaceSelectedWord(), onPressed: () => _replaceSelectedWord(),
icon: const Icon(Icons.find_replace), icon: const Icon(Icons.find_replace),
iconSize: _iconSize, iconSize: _iconSize,

View file

@ -46,7 +46,7 @@ class SearchService {
if (pattern.isEmpty) return; if (pattern.isEmpty) return;
//traversing all the nodes //traversing all the nodes
for (final n in _getAllTextNodes()) { for (final n in _getAllNodes()) {
//matches list will contain the offsets where the desired word, //matches list will contain the offsets where the desired word,
//is found. //is found.
List<int> matches = List<int> matches =
@ -56,19 +56,14 @@ class SearchService {
for (int matchedOffset in matches) { for (int matchedOffset in matches) {
matchedPositions.add(Position(path: n.path, offset: matchedOffset)); 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; selectedIndex = -1;
} }
List<Node> _getAllTextNodes() { List<Node> _getAllNodes() {
final contents = editorState.document.root.children; final contents = editorState.document.root.children;
if (contents.isEmpty) return []; if (contents.isEmpty) return [];
@ -93,22 +88,15 @@ class SearchService {
return nodes; return nodes;
} }
/// This method takes in the TextNode's path, matches is a list of offsets, void _highlightAllMatches(
/// 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,
int patternLength, { int patternLength, {
bool unhighlight = false, bool unhighlight = false,
}) { }) {
for (final match in matches) { for (final match in matchedPositions) {
final start = Position(path: path, offset: match); final start = Position(path: match.path, offset: match.offset);
final end = Position( final end = Position(
path: start.path, path: match.path,
offset: start.offset + queriedPattern.length, offset: match.offset + patternLength,
); );
final selection = Selection(start: start, end: end); final selection = Selection(start: start, end: end);
@ -178,7 +166,7 @@ class SearchService {
/// Replaces the current selected word with replaceText. /// Replaces the current selected word with replaceText.
/// After replacing the selected word, this method selects the next /// After replacing the selected word, this method selects the next
/// matched word if that exists. /// matched word if that exists.
void replaceSelectedWord(String replaceText, [bool fromFirst = false]) { void replaceSelectedWord(String replaceText) {
if (replaceText.isEmpty || if (replaceText.isEmpty ||
queriedPattern.isEmpty || queriedPattern.isEmpty ||
matchedPositions.isEmpty) { matchedPositions.isEmpty) {
@ -189,8 +177,7 @@ class SearchService {
selectedIndex++; selectedIndex++;
} }
final position = final position = matchedPositions[selectedIndex];
fromFirst ? matchedPositions.first : matchedPositions[selectedIndex];
_selectWordAtPosition(position); _selectWordAtPosition(position);
//unhighlight the selected word before it is replaced //unhighlight the selected word before it is replaced
@ -202,10 +189,8 @@ class SearchService {
editorState.undoManager.forgetRecentUndo(); editorState.undoManager.forgetRecentUndo();
final textNode = editorState.getNodeAtPath(position.path)!; final textNode = editorState.getNodeAtPath(position.path)!;
final transaction = editorState.transaction
final transaction = editorState.transaction; ..replaceText(
transaction.replaceText(
textNode, textNode,
position.offset, position.offset,
queriedPattern.length, queriedPattern.length,
@ -214,36 +199,33 @@ class SearchService {
editorState.apply(transaction); editorState.apply(transaction);
if (fromFirst) { matchedPositions.clear();
matchedPositions.removeAt(0); findAndHighlight(queriedPattern);
} 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 /// Replaces all the found occurances of pattern with replaceText
void replaceAllMatches(String replaceText) { void replaceAllMatches(String replaceText) {
if (replaceText.isEmpty || queriedPattern.isEmpty) { if (replaceText.isEmpty ||
queriedPattern.isEmpty ||
matchedPositions.isEmpty) {
return; 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++) { _highlightAllMatches(queriedPattern.length, unhighlight: true);
replaceSelectedWord(replaceText, 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( void _applySelectedHighlightColor(

View file

@ -197,6 +197,44 @@ void main() async {
await editor.dispose(); 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 { testWidgets('replace all on found matches', (tester) async {
const patternToBeFound = 'Welcome'; const patternToBeFound = 'Welcome';
const replacePattern = 'Salute'; const replacePattern = 'Salute';
@ -239,5 +277,39 @@ void main() async {
} }
await editor.dispose(); 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(); 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( Future<void> pressFindAndReplaceCommand(
TestableEditor editor, { TestableEditor editor, {
bool openReplace = false, bool openReplace = false,