diff --git a/src/Finder.cpp b/src/Finder.cpp index fe5d3e0b3..64b8664b3 100644 --- a/src/Finder.cpp +++ b/src/Finder.cpp @@ -46,6 +46,17 @@ void Finder::setSearchText(const QString &text) this->text = text; } +void Finder::setSelectionRange(Sci_CharacterRange range) +{ + use_selection_range = true; + selection_range = range; +} + +void Finder::clearSelectionRange() +{ + use_selection_range = false; +} + Sci_CharacterRange Finder::findNext(int startPos) { did_latest_search_wrap = false; @@ -53,9 +64,34 @@ Sci_CharacterRange Finder::findNext(int startPos) if (text.isEmpty()) return {INVALID_POSITION, INVALID_POSITION}; - const int pos = startPos == INVALID_POSITION ? editor->selectionEnd() : startPos; const QByteArray textData = text.toUtf8(); + if (use_selection_range) { + // Clamp start position to within the selection + const int rangeEnd = selection_range.cpMax; + int pos = startPos == INVALID_POSITION ? editor->selectionEnd() : startPos; + if (pos < selection_range.cpMin) pos = selection_range.cpMin; + if (pos > rangeEnd) pos = rangeEnd; + + editor->setTargetRange(pos, rangeEnd); + editor->setSearchFlags(search_flags); + + if (editor->searchInTarget(textData.length(), textData.constData()) != INVALID_POSITION) { + return {static_cast(editor->targetStart()), static_cast(editor->targetEnd())}; + } + // Wrap within selection + if (wrap && pos > selection_range.cpMin) { + editor->setTargetRange(selection_range.cpMin, pos); + if (editor->searchInTarget(textData.length(), textData.constData()) != INVALID_POSITION) { + did_latest_search_wrap = true; + return {static_cast(editor->targetStart()), static_cast(editor->targetEnd())}; + } + } + return {INVALID_POSITION, INVALID_POSITION}; + } + + const int pos = startPos == INVALID_POSITION ? editor->selectionEnd() : startPos; + editor->setTargetRange(pos, editor->length()); editor->setSearchFlags(search_flags); @@ -81,9 +117,32 @@ Sci_CharacterRange Finder::findPrev() if (text.isEmpty()) return {INVALID_POSITION, INVALID_POSITION}; - const int pos = editor->selectionStart(); const QByteArray textData = text.toUtf8(); + if (use_selection_range) { + int pos = editor->selectionStart(); + if (pos > selection_range.cpMax) pos = selection_range.cpMax; + if (pos < selection_range.cpMin) pos = selection_range.cpMin; + + editor->setSearchFlags(search_flags); + auto range = editor->findText(search_flags, textData.constData(), pos, selection_range.cpMin); + + if (range.first != INVALID_POSITION) { + return {static_cast(range.first), static_cast(range.second)}; + } + // Wrap within selection + if (wrap && pos < selection_range.cpMax) { + range = editor->findText(search_flags, textData.constData(), selection_range.cpMax, pos); + if (range.first != INVALID_POSITION) { + did_latest_search_wrap = true; + return {static_cast(range.first), static_cast(range.second)}; + } + } + return {INVALID_POSITION, INVALID_POSITION}; + } + + const int pos = editor->selectionStart(); + editor->setTargetRange(pos, editor->length()); editor->setSearchFlags(search_flags); @@ -104,17 +163,21 @@ Sci_CharacterRange Finder::findPrev() return {INVALID_POSITION, INVALID_POSITION}; } -// Count all occurrences in the document +// Count all occurrences in the document (or selection if set) int Finder::count() { int total = 0; if (text.length() > 0) { - forEachMatch([&](int start, int end) { + auto counter = [&](int start, int end) { Q_UNUSED(start); total++; return end; - }); + }; + if (use_selection_range) + forEachMatchInRange(counter, selection_range); + else + forEachMatch(counter); } return total; @@ -123,22 +186,33 @@ int Finder::count() Sci_CharacterRange Finder::replaceSelectionIfMatch(const QString &replaceText) { const QByteArray textData = text.toUtf8(); - bool isRegex = editor->searchFlags() & SCFIND_REGEXP; + const bool isRegex = search_flags & SCFIND_REGEXP; + const Sci_PositionCR selectionStart = editor->selectionStart(); + const Sci_PositionCR selectionEnd = editor->selectionEnd(); + + if (use_selection_range && (selectionStart < selection_range.cpMin || selectionEnd > selection_range.cpMax)) + return {INVALID_POSITION, INVALID_POSITION}; // Search just in the selection to see if the current selection is a match - editor->setTargetStart(editor->selectionStart()); - editor->setTargetEnd(editor->selectionEnd()); + editor->setTargetStart(selectionStart); + editor->setTargetEnd(selectionEnd); editor->setSearchFlags(search_flags); if (editor->searchInTarget(textData.length(), textData.constData()) != INVALID_POSITION) { const QByteArray replaceData = replaceText.toUtf8(); + const Sci_PositionCR matchStart = editor->targetStart(); + const Sci_PositionCR matchEnd = editor->targetEnd(); + int replaceLength; if (isRegex) - editor->replaceTargetRE(replaceData.length(), replaceData.constData()); + replaceLength = editor->replaceTargetRE(replaceData.length(), replaceData.constData()); else - editor->replaceTarget(replaceData.length(), replaceData.constData()); + replaceLength = editor->replaceTarget(replaceData.length(), replaceData.constData()); - return {static_cast(editor->targetStart()), static_cast(editor->targetEnd())}; + if (use_selection_range) + selection_range.cpMax += replaceLength - (matchEnd - matchStart); + + return {matchStart, static_cast(matchStart + replaceLength)}; } return {INVALID_POSITION, INVALID_POSITION}; @@ -152,7 +226,9 @@ int Finder::replaceAll(const QString &replaceText) const QByteArray &replaceData = replaceText.toUtf8(); const QByteArray &b = text.toUtf8(); const char *c = b.constData(); - Sci_TextToFind ttf {{0, (Sci_PositionCR)editor->length()}, c, {-1, -1}}; + const Sci_PositionCR rangeStart = use_selection_range ? selection_range.cpMin : 0; + Sci_PositionCR rangeEnd = use_selection_range ? selection_range.cpMax : (Sci_PositionCR)editor->length(); + Sci_TextToFind ttf {{rangeStart, rangeEnd}, c, {-1, -1}}; const bool isRegex = search_flags & SCFIND_REGEXP; int total = 0; @@ -173,11 +249,22 @@ int Finder::replaceAll(const QString &replaceText) else ttf.chrg.cpMin = start + editor->replaceTarget(replaceData.length(), replaceData.constData()); - // The replace could have changed the document size, so update the end of the search range - ttf.chrg.cpMax = editor->length(); + // Update the end of the search range based on the length delta of this replacement. + // When confined to a selection, track the selection boundary precisely; otherwise + // expand to the full (possibly grown) document length. + if (use_selection_range) { + rangeEnd += ttf.chrg.cpMin - end; // delta = newLength - oldMatchLength + ttf.chrg.cpMax = rangeEnd; + } else { + ttf.chrg.cpMax = editor->length(); + } total++; } + // Keep the stored selection range in sync with the post-replacement boundary + if (use_selection_range) + selection_range.cpMax = rangeEnd; + return total; } diff --git a/src/Finder.h b/src/Finder.h index 4f091eb0c..b14b1f9dd 100644 --- a/src/Finder.h +++ b/src/Finder.h @@ -41,6 +41,10 @@ class Finder Sci_CharacterRange replaceSelectionIfMatch(const QString &replaceText); int replaceAll(const QString &replaceText); + void setSelectionRange(Sci_CharacterRange range); + void clearSelectionRange(); + bool hasSelectionRange() const { return use_selection_range; } + template void forEachMatch(Func callback) { forEachMatchInRange(callback, {0, (Sci_PositionCR)editor->length()}); } @@ -54,6 +58,9 @@ class Finder bool wrap = false; int search_flags = 0; QString text; + + bool use_selection_range = false; + Sci_CharacterRange selection_range = {0, 0}; }; diff --git a/src/dialogs/FindReplaceDialog.cpp b/src/dialogs/FindReplaceDialog.cpp index dc95b0da8..7bd6b4bcb 100644 --- a/src/dialogs/FindReplaceDialog.cpp +++ b/src/dialogs/FindReplaceDialog.cpp @@ -82,6 +82,16 @@ FindReplaceDialog::FindReplaceDialog(ISearchResultsHandler *searchResults, MainW connect(this, &FindReplaceDialog::windowActivated, [=]() { ui->comboFind->setFocus(); ui->comboFind->lineEdit()->selectAll(); + updateInSelectionCheckbox(); + }); + + // "In selection" checkbox: capture selection bounds when checked + ui->checkBoxInSelection->setEnabled(false); + connect(ui->checkBoxInSelection, &QCheckBox::toggled, this, [=](bool checked) { + if (checked) { + capturedSelectionRange = {static_cast(editor->selectionStart()), + static_cast(editor->selectionEnd())}; + } }); connect(this, &QDialog::rejected, [=]() { @@ -195,6 +205,8 @@ void FindReplaceDialog::showEvent(QShowEvent *event) isFirstTime = false; + updateInSelectionCheckbox(); + QDialog::showEvent(event); } @@ -267,7 +279,8 @@ void FindReplaceDialog::findAllInCurrentDocument() QString text = findString(); finder->setSearchText(text); - finder->forEachMatch([&](int start, int end){ + + auto matchHandler = [&](int start, int end){ // Only add the file entry if there was a valid search result if (firstMatch) { searchResultsHandler->newFileEntry(editor); @@ -284,7 +297,12 @@ void FindReplaceDialog::findAllInCurrentDocument() searchResultsHandler->newResultsEntry(lineText, line, startPositionFromBeginning, endPositionFromBeginning); return end; - }); + }; + + if (finder->hasSelectionRange()) + finder->forEachMatchInRange(matchHandler, capturedSelectionRange); + else + finder->forEachMatch(matchHandler); } void FindReplaceDialog::findAllInDocuments() @@ -314,9 +332,15 @@ void FindReplaceDialog::replace() convertToExtended(replaceText); } + const bool inSelection = ui->checkBoxInSelection->isChecked() && ui->checkBoxInSelection->isEnabled(); + const int originalDocLength = inSelection ? editor->length() : 0; + Sci_CharacterRange range = finder->replaceSelectionIfMatch(replaceText); if (ScintillaNext::isRangeValid(range)) { + if (inSelection) + capturedSelectionRange.cpMax += editor->length() - originalDocLength; + showMessage(tr("1 occurrence was replaced"), "blue"); } @@ -344,8 +368,24 @@ void FindReplaceDialog::replaceAll() convertToExtended(replaceText); } + const bool inSelection = ui->checkBoxInSelection->isChecked() && ui->checkBoxInSelection->isEnabled(); + const int originalDocLength = inSelection ? editor->length() : 0; + int count = finder->replaceAll(replaceText); showMessage(tr("Replaced %Ln matches", "", count), "green"); + + // After replacing in selection, re-apply a selection covering the modified region + if (inSelection) { + const int lengthDelta = editor->length() - originalDocLength; + const int newEnd = capturedSelectionRange.cpMax + lengthDelta; + editor->setSelection(newEnd, capturedSelectionRange.cpMin); + capturedSelectionRange.cpMax = newEnd; + // If the selection collapsed (all text deleted), uncheck + if (newEnd == capturedSelectionRange.cpMin) { + ui->checkBoxInSelection->setChecked(false); + updateInSelectionCheckbox(); + } + } } void FindReplaceDialog::count() @@ -364,6 +404,12 @@ void FindReplaceDialog::setEditor(ScintillaNext *editor) this->editor = editor; finder->setEditor(editor); + + // Reset the in-selection state when the editor changes + capturedSelectionRange = {0, 0}; + ui->checkBoxInSelection->setChecked(false); + ui->checkBoxInSelection->setEnabled(false); + finder->clearSelectionRange(); } void FindReplaceDialog::performNextSearch() @@ -553,6 +599,12 @@ void FindReplaceDialog::prepareToPerformSearch(bool replace) finder->setWrap(ui->checkBoxWrapAround->isChecked()); finder->setSearchFlags(computeSearchFlags()); finder->setSearchText(findText); + + if (ui->checkBoxInSelection->isChecked() && ui->checkBoxInSelection->isEnabled()) { + finder->setSelectionRange(capturedSelectionRange); + } else { + finder->clearSelectionRange(); + } } void FindReplaceDialog::loadSettings() @@ -666,6 +718,33 @@ void FindReplaceDialog::restorePosition() } } +void FindReplaceDialog::updateInSelectionCheckbox() +{ + if (!editor) { + ui->checkBoxInSelection->setEnabled(false); + return; + } + + const int selStart = editor->selectionStart(); + const int selEnd = editor->selectionEnd(); + const bool hasSelection = (selStart != selEnd); + const bool isRectangular = editor->selectionIsRectangle(); + const bool isMultiple = (editor->selections() > 1); + + if (hasSelection && !isRectangular && !isMultiple) { + ui->checkBoxInSelection->setEnabled(true); + // Capture the current selection bounds (only update if not already checked) + if (!ui->checkBoxInSelection->isChecked()) { + capturedSelectionRange = {static_cast(selStart), + static_cast(selEnd)}; + } + } else { + ui->checkBoxInSelection->setChecked(false); + ui->checkBoxInSelection->setEnabled(false); + finder->clearSelectionRange(); + } +} + int FindReplaceDialog::computeSearchFlags() { int flags = 0; @@ -719,8 +798,14 @@ void FindReplaceDialog::markAll() editor->setIndicatorCurrent(markIndicator); + const bool inSelection = finder->hasSelectionRange(); + if (ui->checkBoxPurgeForEachSearch->isChecked()) { - editor->indicatorClearRange(0, editor->length()); + if (inSelection) + editor->indicatorClearRange(capturedSelectionRange.cpMin, + capturedSelectionRange.cpMax - capturedSelectionRange.cpMin); + else + editor->indicatorClearRange(0, editor->length()); clearAllBookmarks(); } @@ -730,7 +815,7 @@ void FindReplaceDialog::markAll() } int count = 0; - finder->forEachMatch([&](int start, int end) { + auto callback = [&](int start, int end) { editor->indicatorFillRange(start, end - start); count++; @@ -742,9 +827,15 @@ void FindReplaceDialog::markAll() } return end; - }); + }; - showMessage(tr("Mark: %Ln match in entire file", "", count), "green"); + if (inSelection) { + finder->forEachMatchInRange(callback, capturedSelectionRange); + showMessage(tr("Mark: %Ln match in selection", "", count), "green"); + } else { + finder->forEachMatch(callback); + showMessage(tr("Mark: %Ln match in entire file", "", count), "green"); + } } void FindReplaceDialog::clearAllMarks() diff --git a/src/dialogs/FindReplaceDialog.h b/src/dialogs/FindReplaceDialog.h index fe9fc6811..3c2431a8b 100644 --- a/src/dialogs/FindReplaceDialog.h +++ b/src/dialogs/FindReplaceDialog.h @@ -109,6 +109,8 @@ private slots: void updateFindList(const QString &text); void updateReplaceList(const QString &text); + void updateInSelectionCheckbox(); + bool isFirstTime = true; QPoint lastClosedPosition; Ui::FindReplaceDialog *ui; @@ -118,6 +120,8 @@ private slots: QTabBar *tabBar; ISearchResultsHandler *searchResultsHandler; Finder *finder; + + Sci_CharacterRange capturedSelectionRange = {0, 0}; }; #endif // FINDREPLACEDIALOG_H diff --git a/src/dialogs/FindReplaceDialog.ui b/src/dialogs/FindReplaceDialog.ui index b585c2d43..66e4bdd95 100644 --- a/src/dialogs/FindReplaceDialog.ui +++ b/src/dialogs/FindReplaceDialog.ui @@ -511,6 +511,13 @@ + + + + In &selection + + + @@ -526,6 +533,8 @@ radioExtendedSearch radioRegexSearch checkBoxRegexMatchesNewline + checkBoxWrapAround + checkBoxInSelection buttonFind buttonCount buttonReplace