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:
parent
a9af2bbd37
commit
e38b07d13b
4 changed files with 121 additions and 56 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue