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(
|
||||
key: const Key('replaceSelectedButton'),
|
||||
onPressed: () => _replaceSelectedWord(),
|
||||
icon: const Icon(Icons.find_replace),
|
||||
iconSize: _iconSize,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue