Skip to content

Commit 5f51233

Browse files
committed
Add Find/Replace In Selection
1 parent c53aab6 commit 5f51233

5 files changed

Lines changed: 202 additions & 16 deletions

File tree

src/Finder.cpp

Lines changed: 101 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,52 @@ void Finder::setSearchText(const QString &text)
4646
this->text = text;
4747
}
4848

49+
void Finder::setSelectionRange(Sci_CharacterRange range)
50+
{
51+
use_selection_range = true;
52+
selection_range = range;
53+
}
54+
55+
void Finder::clearSelectionRange()
56+
{
57+
use_selection_range = false;
58+
}
59+
4960
Sci_CharacterRange Finder::findNext(int startPos)
5061
{
5162
did_latest_search_wrap = false;
5263

5364
if (text.isEmpty())
5465
return {INVALID_POSITION, INVALID_POSITION};
5566

56-
const int pos = startPos == INVALID_POSITION ? editor->selectionEnd() : startPos;
5767
const QByteArray textData = text.toUtf8();
5868

69+
if (use_selection_range) {
70+
// Clamp start position to within the selection
71+
const int rangeEnd = selection_range.cpMax;
72+
int pos = startPos == INVALID_POSITION ? editor->selectionEnd() : startPos;
73+
if (pos < selection_range.cpMin) pos = selection_range.cpMin;
74+
if (pos > rangeEnd) pos = rangeEnd;
75+
76+
editor->setTargetRange(pos, rangeEnd);
77+
editor->setSearchFlags(search_flags);
78+
79+
if (editor->searchInTarget(textData.length(), textData.constData()) != INVALID_POSITION) {
80+
return {static_cast<Sci_PositionCR>(editor->targetStart()), static_cast<Sci_PositionCR>(editor->targetEnd())};
81+
}
82+
// Wrap within selection
83+
if (wrap && pos > selection_range.cpMin) {
84+
editor->setTargetRange(selection_range.cpMin, pos);
85+
if (editor->searchInTarget(textData.length(), textData.constData()) != INVALID_POSITION) {
86+
did_latest_search_wrap = true;
87+
return {static_cast<Sci_PositionCR>(editor->targetStart()), static_cast<Sci_PositionCR>(editor->targetEnd())};
88+
}
89+
}
90+
return {INVALID_POSITION, INVALID_POSITION};
91+
}
92+
93+
const int pos = startPos == INVALID_POSITION ? editor->selectionEnd() : startPos;
94+
5995
editor->setTargetRange(pos, editor->length());
6096
editor->setSearchFlags(search_flags);
6197

@@ -81,9 +117,32 @@ Sci_CharacterRange Finder::findPrev()
81117
if (text.isEmpty())
82118
return {INVALID_POSITION, INVALID_POSITION};
83119

84-
const int pos = editor->selectionStart();
85120
const QByteArray textData = text.toUtf8();
86121

122+
if (use_selection_range) {
123+
int pos = editor->selectionStart();
124+
if (pos > selection_range.cpMax) pos = selection_range.cpMax;
125+
if (pos < selection_range.cpMin) pos = selection_range.cpMin;
126+
127+
editor->setSearchFlags(search_flags);
128+
auto range = editor->findText(search_flags, textData.constData(), pos, selection_range.cpMin);
129+
130+
if (range.first != INVALID_POSITION) {
131+
return {static_cast<Sci_PositionCR>(range.first), static_cast<Sci_PositionCR>(range.second)};
132+
}
133+
// Wrap within selection
134+
if (wrap && pos < selection_range.cpMax) {
135+
range = editor->findText(search_flags, textData.constData(), selection_range.cpMax, pos);
136+
if (range.first != INVALID_POSITION) {
137+
did_latest_search_wrap = true;
138+
return {static_cast<Sci_PositionCR>(range.first), static_cast<Sci_PositionCR>(range.second)};
139+
}
140+
}
141+
return {INVALID_POSITION, INVALID_POSITION};
142+
}
143+
144+
const int pos = editor->selectionStart();
145+
87146
editor->setTargetRange(pos, editor->length());
88147
editor->setSearchFlags(search_flags);
89148

@@ -104,17 +163,21 @@ Sci_CharacterRange Finder::findPrev()
104163
return {INVALID_POSITION, INVALID_POSITION};
105164
}
106165

107-
// Count all occurrences in the document
166+
// Count all occurrences in the document (or selection if set)
108167
int Finder::count()
109168
{
110169
int total = 0;
111170

112171
if (text.length() > 0) {
113-
forEachMatch([&](int start, int end) {
172+
auto counter = [&](int start, int end) {
114173
Q_UNUSED(start);
115174
total++;
116175
return end;
117-
});
176+
};
177+
if (use_selection_range)
178+
forEachMatchInRange(counter, selection_range);
179+
else
180+
forEachMatch(counter);
118181
}
119182

120183
return total;
@@ -123,22 +186,33 @@ int Finder::count()
123186
Sci_CharacterRange Finder::replaceSelectionIfMatch(const QString &replaceText)
124187
{
125188
const QByteArray textData = text.toUtf8();
126-
bool isRegex = editor->searchFlags() & SCFIND_REGEXP;
189+
const bool isRegex = search_flags & SCFIND_REGEXP;
190+
const Sci_PositionCR selectionStart = editor->selectionStart();
191+
const Sci_PositionCR selectionEnd = editor->selectionEnd();
192+
193+
if (use_selection_range && (selectionStart < selection_range.cpMin || selectionEnd > selection_range.cpMax))
194+
return {INVALID_POSITION, INVALID_POSITION};
127195

128196
// Search just in the selection to see if the current selection is a match
129-
editor->setTargetStart(editor->selectionStart());
130-
editor->setTargetEnd(editor->selectionEnd());
197+
editor->setTargetStart(selectionStart);
198+
editor->setTargetEnd(selectionEnd);
131199
editor->setSearchFlags(search_flags);
132200

133201
if (editor->searchInTarget(textData.length(), textData.constData()) != INVALID_POSITION) {
134202
const QByteArray replaceData = replaceText.toUtf8();
203+
const Sci_PositionCR matchStart = editor->targetStart();
204+
const Sci_PositionCR matchEnd = editor->targetEnd();
205+
int replaceLength;
135206

136207
if (isRegex)
137-
editor->replaceTargetRE(replaceData.length(), replaceData.constData());
208+
replaceLength = editor->replaceTargetRE(replaceData.length(), replaceData.constData());
138209
else
139-
editor->replaceTarget(replaceData.length(), replaceData.constData());
210+
replaceLength = editor->replaceTarget(replaceData.length(), replaceData.constData());
140211

141-
return {static_cast<Sci_PositionCR>(editor->targetStart()), static_cast<Sci_PositionCR>(editor->targetEnd())};
212+
if (use_selection_range)
213+
selection_range.cpMax += replaceLength - (matchEnd - matchStart);
214+
215+
return {matchStart, static_cast<Sci_PositionCR>(matchStart + replaceLength)};
142216
}
143217

144218
return {INVALID_POSITION, INVALID_POSITION};
@@ -152,7 +226,9 @@ int Finder::replaceAll(const QString &replaceText)
152226
const QByteArray &replaceData = replaceText.toUtf8();
153227
const QByteArray &b = text.toUtf8();
154228
const char *c = b.constData();
155-
Sci_TextToFind ttf {{0, (Sci_PositionCR)editor->length()}, c, {-1, -1}};
229+
const Sci_PositionCR rangeStart = use_selection_range ? selection_range.cpMin : 0;
230+
Sci_PositionCR rangeEnd = use_selection_range ? selection_range.cpMax : (Sci_PositionCR)editor->length();
231+
Sci_TextToFind ttf {{rangeStart, rangeEnd}, c, {-1, -1}};
156232
const bool isRegex = search_flags & SCFIND_REGEXP;
157233
int total = 0;
158234

@@ -173,11 +249,22 @@ int Finder::replaceAll(const QString &replaceText)
173249
else
174250
ttf.chrg.cpMin = start + editor->replaceTarget(replaceData.length(), replaceData.constData());
175251

176-
// The replace could have changed the document size, so update the end of the search range
177-
ttf.chrg.cpMax = editor->length();
252+
// Update the end of the search range based on the length delta of this replacement.
253+
// When confined to a selection, track the selection boundary precisely; otherwise
254+
// expand to the full (possibly grown) document length.
255+
if (use_selection_range) {
256+
rangeEnd += ttf.chrg.cpMin - end; // delta = newLength - oldMatchLength
257+
ttf.chrg.cpMax = rangeEnd;
258+
} else {
259+
ttf.chrg.cpMax = editor->length();
260+
}
178261

179262
total++;
180263
}
181264

265+
// Keep the stored selection range in sync with the post-replacement boundary
266+
if (use_selection_range)
267+
selection_range.cpMax = rangeEnd;
268+
182269
return total;
183270
}

src/Finder.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ class Finder
4141
Sci_CharacterRange replaceSelectionIfMatch(const QString &replaceText);
4242
int replaceAll(const QString &replaceText);
4343

44+
void setSelectionRange(Sci_CharacterRange range);
45+
void clearSelectionRange();
46+
bool hasSelectionRange() const { return use_selection_range; }
47+
4448
template<typename Func>
4549
void forEachMatch(Func callback) { forEachMatchInRange(callback, {0, (Sci_PositionCR)editor->length()}); }
4650

@@ -54,6 +58,9 @@ class Finder
5458
bool wrap = false;
5559
int search_flags = 0;
5660
QString text;
61+
62+
bool use_selection_range = false;
63+
Sci_CharacterRange selection_range = {0, 0};
5764
};
5865

5966

src/dialogs/FindReplaceDialog.cpp

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ FindReplaceDialog::FindReplaceDialog(ISearchResultsHandler *searchResults, MainW
7878
connect(this, &FindReplaceDialog::windowActivated, [=]() {
7979
ui->comboFind->setFocus();
8080
ui->comboFind->lineEdit()->selectAll();
81+
updateInSelectionCheckbox();
82+
});
83+
84+
// "In selection" checkbox: capture selection bounds when checked
85+
ui->checkBoxInSelection->setEnabled(false);
86+
connect(ui->checkBoxInSelection, &QCheckBox::toggled, this, [=](bool checked) {
87+
if (checked) {
88+
capturedSelectionRange = {static_cast<Sci_PositionCR>(editor->selectionStart()),
89+
static_cast<Sci_PositionCR>(editor->selectionEnd())};
90+
}
8191
});
8292

8393
connect(this, &QDialog::rejected, [=]() {
@@ -188,6 +198,8 @@ void FindReplaceDialog::showEvent(QShowEvent *event)
188198

189199
isFirstTime = false;
190200

201+
updateInSelectionCheckbox();
202+
191203
QDialog::showEvent(event);
192204
}
193205

@@ -260,7 +272,8 @@ void FindReplaceDialog::findAllInCurrentDocument()
260272
QString text = findString();
261273

262274
finder->setSearchText(text);
263-
finder->forEachMatch([&](int start, int end){
275+
276+
auto matchHandler = [&](int start, int end){
264277
// Only add the file entry if there was a valid search result
265278
if (firstMatch) {
266279
searchResultsHandler->newFileEntry(editor);
@@ -277,7 +290,12 @@ void FindReplaceDialog::findAllInCurrentDocument()
277290
searchResultsHandler->newResultsEntry(lineText, line, startPositionFromBeginning, endPositionFromBeginning);
278291

279292
return end;
280-
});
293+
};
294+
295+
if (finder->hasSelectionRange())
296+
finder->forEachMatchInRange(matchHandler, capturedSelectionRange);
297+
else
298+
finder->forEachMatch(matchHandler);
281299
}
282300

283301
void FindReplaceDialog::findAllInDocuments()
@@ -307,9 +325,15 @@ void FindReplaceDialog::replace()
307325
convertToExtended(replaceText);
308326
}
309327

328+
const bool inSelection = ui->checkBoxInSelection->isChecked() && ui->checkBoxInSelection->isEnabled();
329+
const int originalDocLength = inSelection ? editor->length() : 0;
330+
310331
Sci_CharacterRange range = finder->replaceSelectionIfMatch(replaceText);
311332

312333
if (ScintillaNext::isRangeValid(range)) {
334+
if (inSelection)
335+
capturedSelectionRange.cpMax += editor->length() - originalDocLength;
336+
313337
showMessage(tr("1 occurrence was replaced"), "blue");
314338
}
315339

@@ -337,8 +361,24 @@ void FindReplaceDialog::replaceAll()
337361
convertToExtended(replaceText);
338362
}
339363

364+
const bool inSelection = ui->checkBoxInSelection->isChecked() && ui->checkBoxInSelection->isEnabled();
365+
const int originalDocLength = inSelection ? editor->length() : 0;
366+
340367
int count = finder->replaceAll(replaceText);
341368
showMessage(tr("Replaced %Ln matches", "", count), "green");
369+
370+
// After replacing in selection, re-apply a selection covering the modified region
371+
if (inSelection) {
372+
const int lengthDelta = editor->length() - originalDocLength;
373+
const int newEnd = capturedSelectionRange.cpMax + lengthDelta;
374+
editor->setSelection(newEnd, capturedSelectionRange.cpMin);
375+
capturedSelectionRange.cpMax = newEnd;
376+
// If the selection collapsed (all text deleted), uncheck
377+
if (newEnd == capturedSelectionRange.cpMin) {
378+
ui->checkBoxInSelection->setChecked(false);
379+
updateInSelectionCheckbox();
380+
}
381+
}
342382
}
343383

344384
void FindReplaceDialog::count()
@@ -357,6 +397,12 @@ void FindReplaceDialog::setEditor(ScintillaNext *editor)
357397
this->editor = editor;
358398

359399
finder->setEditor(editor);
400+
401+
// Reset the in-selection state when the editor changes
402+
capturedSelectionRange = {0, 0};
403+
ui->checkBoxInSelection->setChecked(false);
404+
ui->checkBoxInSelection->setEnabled(false);
405+
finder->clearSelectionRange();
360406
}
361407

362408
void FindReplaceDialog::performNextSearch()
@@ -500,6 +546,12 @@ void FindReplaceDialog::prepareToPerformSearch(bool replace)
500546
finder->setWrap(ui->checkBoxWrapAround->isChecked());
501547
finder->setSearchFlags(computeSearchFlags());
502548
finder->setSearchText(findText);
549+
550+
if (ui->checkBoxInSelection->isChecked() && ui->checkBoxInSelection->isEnabled()) {
551+
finder->setSelectionRange(capturedSelectionRange);
552+
} else {
553+
finder->clearSelectionRange();
554+
}
503555
}
504556

505557
void FindReplaceDialog::loadSettings()
@@ -613,6 +665,33 @@ void FindReplaceDialog::restorePosition()
613665
}
614666
}
615667

668+
void FindReplaceDialog::updateInSelectionCheckbox()
669+
{
670+
if (!editor) {
671+
ui->checkBoxInSelection->setEnabled(false);
672+
return;
673+
}
674+
675+
const int selStart = editor->selectionStart();
676+
const int selEnd = editor->selectionEnd();
677+
const bool hasSelection = (selStart != selEnd);
678+
const bool isRectangular = editor->selectionIsRectangle();
679+
const bool isMultiple = (editor->selections() > 1);
680+
681+
if (hasSelection && !isRectangular && !isMultiple) {
682+
ui->checkBoxInSelection->setEnabled(true);
683+
// Capture the current selection bounds (only update if not already checked)
684+
if (!ui->checkBoxInSelection->isChecked()) {
685+
capturedSelectionRange = {static_cast<Sci_PositionCR>(selStart),
686+
static_cast<Sci_PositionCR>(selEnd)};
687+
}
688+
} else {
689+
ui->checkBoxInSelection->setChecked(false);
690+
ui->checkBoxInSelection->setEnabled(false);
691+
finder->clearSelectionRange();
692+
}
693+
}
694+
616695
int FindReplaceDialog::computeSearchFlags()
617696
{
618697
int flags = 0;

src/dialogs/FindReplaceDialog.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ private slots:
103103
void updateFindList(const QString &text);
104104
void updateReplaceList(const QString &text);
105105

106+
void updateInSelectionCheckbox();
107+
106108
bool isFirstTime = true;
107109
QPoint lastClosedPosition;
108110
Ui::FindReplaceDialog *ui;
@@ -112,6 +114,8 @@ private slots:
112114
QTabBar *tabBar;
113115
ISearchResultsHandler *searchResultsHandler;
114116
Finder *finder;
117+
118+
Sci_CharacterRange capturedSelectionRange = {0, 0};
115119
};
116120

117121
#endif // FINDREPLACEDIALOG_H

0 commit comments

Comments
 (0)