[LyX/2.5.x] Improve simple search within selection
Juergen Spitzmueller
spitz at lyx.org
Mon May 18 17:46:09 UTC 2026
commit c60ec0a3009955b049d9303855db59e6cb0a2327
Author: Juergen Spitzmueller <spitz at lyx.org>
Date: Sat Mar 28 12:44:10 2026 +0100
Improve simple search within selection
When searching/replacing within a selection, remember the selection range
beyond the first match.
To this end, we record whether a selection has been made in result of
a search match and differentiate this kind of selection from others
(those within which we ought to search).
In result, LyX (simple) search/replace within selection does no longer
work in the restricted way (like LibreOffice) to only provide one hit
within a selection, it keeps on searching for more hits until the
selection end is reached and then asks whether it should continue
searching outside (like M$ Word).
It also does not moan about selection end being reached when it made the
selection itself (to highlight a match).
(cherry picked from commit 643e2bb0f9207f52ca6bee6257ffd0221a089c73)
---
src/BufferView.cpp | 5 +++-
src/BufferView.h | 3 +-
src/Cursor.cpp | 9 ++++--
src/Cursor.h | 6 ++++
src/lyxfind.cpp | 80 +++++++++++++++++++++++++++++++++++-------------------
src/lyxfind.h | 2 +-
status.25x | 3 ++
7 files changed, 74 insertions(+), 34 deletions(-)
diff --git a/src/BufferView.cpp b/src/BufferView.cpp
index f6c672fe02..48bcea3fc3 100644
--- a/src/BufferView.cpp
+++ b/src/BufferView.cpp
@@ -3235,7 +3235,8 @@ bool BufferView::mouseSetCursor(Cursor & cur, bool const select)
void BufferView::putSelectionAt(DocIterator const & cur,
- int length, bool backwards)
+ int length, bool backwards,
+ bool const searchhit)
{
d->cursor_.clearSelection();
@@ -3248,6 +3249,8 @@ void BufferView::putSelectionAt(DocIterator const & cur,
} else
d->cursor_.setSelection(d->cursor_, length);
}
+ if (searchhit)
+ d->cursor_.setSearchMatchSelection(true);
}
diff --git a/src/BufferView.h b/src/BufferView.h
index eca7f84ccc..50f783f37e 100644
--- a/src/BufferView.h
+++ b/src/BufferView.h
@@ -306,7 +306,8 @@ public:
* cur + \c length.
*/
void putSelectionAt(DocIterator const & cur,
- int length, bool backwards);
+ int length, bool backwards,
+ bool const searchhit = false);
/// set a selection between \p from and \p to
void setSelection(DocIterator const & from,
DocIterator const & to);
diff --git a/src/Cursor.cpp b/src/Cursor.cpp
index cc06268483..52c8bf2821 100644
--- a/src/Cursor.cpp
+++ b/src/Cursor.cpp
@@ -123,19 +123,19 @@ DocIterator bruteFind(Cursor const & c, int x, int y)
CursorData::CursorData()
: DocIterator(), anchor_(), selection_(false), mark_(false),
- word_selection_(false), current_font(inherit_font)
+ word_selection_(false), search_match_selection_(false), current_font(inherit_font)
{}
CursorData::CursorData(Buffer * buffer)
: DocIterator(buffer), anchor_(), selection_(false), mark_(false),
- word_selection_(false), current_font(inherit_font)
+ word_selection_(false), search_match_selection_(false), current_font(inherit_font)
{}
CursorData::CursorData(DocIterator const & dit)
: DocIterator(dit), anchor_(), selection_(false), mark_(false),
- word_selection_(false), current_font(inherit_font)
+ word_selection_(false), search_match_selection_(false), current_font(inherit_font)
{}
@@ -251,6 +251,7 @@ CursorSlice CursorData::normalAnchor() const
void CursorData::setSelection()
{
selection(true);
+ setSearchMatchSelection(false);
if (idx() == normalAnchor().idx() &&
pit() == normalAnchor().pit() &&
pos() == normalAnchor().pos())
@@ -262,6 +263,7 @@ void CursorData::setSelection(DocIterator const & where, int n)
{
setCursor(where);
selection(true);
+ setSearchMatchSelection(false);
anchor_ = where;
pos() += n;
}
@@ -500,6 +502,7 @@ void CursorData::clearSelection()
{
selection(false);
setWordSelection(false);
+ setSearchMatchSelection(false);
setMark(false);
resetAnchor();
}
diff --git a/src/Cursor.h b/src/Cursor.h
index c2fcf63135..879dce1033 100644
--- a/src/Cursor.h
+++ b/src/Cursor.h
@@ -123,6 +123,10 @@ public:
bool mark() const { return mark_; }
/// did we place the anchor?
void setMark(bool mark) { mark_ = mark; }
+ /// Set search match selection mode
+ void setSearchMatchSelection(bool set) { search_match_selection_ = set; }
+ /// Are we in search match selection mode?
+ bool searchMatchSelection() const { return search_match_selection_; }
///
void setSelection();
/// set selection at given position
@@ -229,6 +233,8 @@ private:
bool mark_;
/// are we in word-selection mode? This is set when double clicking.
bool word_selection_;
+ /// are we in search match selection mode?
+ bool search_match_selection_;
/// the start of the new born word
DocIterator new_word_;
diff --git a/src/lyxfind.cpp b/src/lyxfind.cpp
index 46180ceea4..62a8fa15fe 100644
--- a/src/lyxfind.cpp
+++ b/src/lyxfind.cpp
@@ -239,8 +239,9 @@ int findForward(DocIterator & cur, DocIterator const & endcur,
bool find_del = true, bool onlysel = false)
{
for (; cur; cur.forwardChar()) {
- if (onlysel && cur.inTexted() && endcur.pit() == cur.pit()
- && endcur.idx() == cur.idx() && endcur.pos() < cur.pos())
+ if (onlysel && cur.inTexted()
+ && (endcur.pit() < cur.pit() || (endcur.pit() == cur.pit()
+ && endcur.idx() == cur.idx() && endcur.pos() < cur.pos())))
break;
if (cur.inTexted()) {
int len = match(cur.paragraph(), cur.pos(), find_del);
@@ -258,8 +259,9 @@ int findBackwards(DocIterator & cur, DocIterator const & endcur,
{
while (cur) {
cur.backwardChar();
- if (onlysel && cur.inTexted() && endcur.pit() == cur.pit()
- && endcur.idx() == cur.idx() && endcur.pos() > cur.pos())
+ if (onlysel && cur.inTexted()
+ && (endcur.pit() > cur.pit() || (endcur.pit() == cur.pit()
+ && endcur.idx() == cur.idx() && endcur.pos() > cur.pos())))
break;
if (cur.inTexted()) {
int len = match(cur.paragraph(), cur.pos(), find_del);
@@ -283,10 +285,13 @@ bool searchAllowed(docstring const & str)
} // namespace
+DocIterator selection_search_start;
+DocIterator selection_search_end;
+
bool findOne(BufferView * bv, docstring const & searchstr,
bool case_sens, bool whole, bool forward,
bool find_del, bool check_wrap, bool const auto_wrap,
- bool instant, bool onlysel)
+ bool instant, bool onlysel, bool fromrep)
{
bool const had_selection = bv->cursor().selection();
@@ -302,10 +307,14 @@ bool findOne(BufferView * bv, docstring const & searchstr,
if (!searchAllowed(searchstr))
return false;
- DocIterator const startcur = bv->cursor().selectionBegin();
- DocIterator const endcur = bv->cursor().selectionEnd();
+ if (!fromrep && (!had_selection || !bv->cursor().searchMatchSelection())) {
+ selection_search_start = bv->cursor().selectionBegin();
+ selection_search_end = bv->cursor().selectionEnd();
+ }
+
+ bool const in_new_selection = onlysel && had_selection && !bv->cursor().searchMatchSelection();
- if (onlysel && had_selection) {
+ if (in_new_selection) {
docstring const matchstring = bv->cursor().selectionAsString(false);
docstring const lcmatchsting = support::lowercase(matchstring);
if (matchstring == searchstr || (!case_sens && lcmatchsting == lowercase(searchstr))) {
@@ -324,28 +333,35 @@ bool findOne(BufferView * bv, docstring const & searchstr,
}
DocIterator cur = forward
- ? ((instant || onlysel) ? bv->cursor().selectionBegin() : bv->cursor().selectionEnd())
- : ((instant || onlysel) ? bv->cursor().selectionEnd() : bv->cursor().selectionBegin());
+ ? ((instant || in_new_selection) ? bv->cursor().selectionBegin() : bv->cursor().selectionEnd())
+ : ((instant || in_new_selection) ? bv->cursor().selectionEnd() : bv->cursor().selectionBegin());
MatchString const match(searchstr, case_sens, whole);
+ // Set only_selection to false if we search outside a set selection range
+ onlysel &= selection_search_start != selection_search_end;
+
int match_len = forward
- ? findForward(cur, endcur, match, find_del, onlysel)
- : findBackwards(cur, startcur, match, find_del, onlysel);
+ ? findForward(cur, selection_search_end, match, find_del, onlysel)
+ : findBackwards(cur, selection_search_start, match, find_del, onlysel);
if (match_len > 0)
- bv->putSelectionAt(cur, match_len, !forward);
- else if (onlysel && had_selection) {
+ bv->putSelectionAt(cur, match_len, !forward, true);
+ else if (onlysel) {
docstring q = _("The search string was not found within the selection.\n"
"Continue search outside?");
int search_answer = frontend::Alert::prompt(_("Search outside selection?"),
q, 0, 1, _("&Yes"), _("&No"));
if (search_answer == 0) {
bv->clearSelection();
+ selection_search_start = bv->cursor().selectionBegin();
+ selection_search_end = bv->cursor().selectionEnd();
if (findOne(bv, searchstr, case_sens, whole, forward,
find_del, check_wrap, auto_wrap, false, false))
return true;
- }
+ } else
+ // restore original selection
+ bv->setSelection(selection_search_start, selection_search_end);
return false;
}
else if (check_wrap) {
@@ -381,12 +397,12 @@ bool findOne(BufferView * bv, docstring const & searchstr,
find_del, false, false, false, false))
return true;
}
- bv->setCursor(startcur);
+ bv->setCursor(selection_search_start);
// restore original selection
if (had_selection) {
bv->cursor().resetAnchor();
- bv->setSelection(startcur, endcur);
+ bv->setSelection(selection_search_start, selection_search_end);
}
return false;
}
@@ -470,18 +486,18 @@ int replaceAll(BufferView * bv,
}
-// the idea here is that we are going to replace the string that
+// The idea here is that we are going to replace the string that
// is selected IF it is the search string.
-// if there is a selection, but it is not the search string, then
-// we basically ignore it. (FIXME We ought to replace only within
-// the selection.)
-// if there is no selection, then:
-// (i) if some search string has been provided, then we find it.
+// If there is a selection, but it is not the search string, then
+// we search for a hit in this selection if "only selection" is set,
+// otherwise we search after the selection.
+// If there is no selection, then:
+// (i) If some search string has been provided, then we find it.
// (think of how the dialog works when you hit "replace" the
// first time.)
-// (ii) if no search string has been provided, then we treat the
-// word the cursor is in as the search string. (why? i have no
-// idea.) but this only works in text?
+// (ii) If no search string has been provided, then we treat the
+// word the cursor is in as the search string. ("Why? I have no
+// idea.) But this only works in text?
//
// returns the number of replacements made (one, if any) and
// whether anything at all was done.
@@ -491,7 +507,8 @@ pair<bool, int> replaceOne(BufferView * bv, docstring searchstr,
bool onlysel)
{
Cursor & cur = bv->cursor();
- if (!cur.selection() || onlysel) {
+ bool const in_new_selection = onlysel && !bv->cursor().searchMatchSelection();
+ if (!cur.selection() || in_new_selection) {
// no selection, non-empty search string: find it
if (!searchstr.empty()) {
bool const found = findOne(bv, searchstr, case_sens, whole,
@@ -535,6 +552,13 @@ pair<bool, int> replaceOne(BufferView * bv, docstring searchstr,
return make_pair(false, 0);
cap::replaceSelectionWithString(cur, replacestr);
+ if (onlysel && selection_search_start != selection_search_end
+ && selection_search_end.pit() == cur.pit()
+ && selection_search_end.idx() == cur.idx()) {
+ selection_search_end.pos() += replacestr.length() - searchstr.length();
+ LASSERT(selection_search_end.pos() >= 0 && selection_search_end.pos() <= cur.lastpos(),
+ selection_search_end.pos() = 0);
+ }
if (forward) {
cur.pos() += replacestr.length();
LASSERT(cur.pos() <= cur.lastpos(),
@@ -542,7 +566,7 @@ pair<bool, int> replaceOne(BufferView * bv, docstring searchstr,
}
if (findnext)
findOne(bv, searchstr, case_sens, whole,
- forward, false, findnext, wrap, false, onlysel);
+ forward, false, findnext, wrap, false, onlysel, true);
return make_pair(true, 1);
}
diff --git a/src/lyxfind.h b/src/lyxfind.h
index 84d2400e2b..bb0c354ace 100644
--- a/src/lyxfind.h
+++ b/src/lyxfind.h
@@ -73,7 +73,7 @@ bool findOne(BufferView * bv, docstring const & searchstr,
bool case_sens, bool whole, bool forward,
bool find_del = true, bool check_wrap = false,
bool const auto_wrap = false, bool instant = false,
- bool onlysel = false);
+ bool onlysel = false, bool fromrep = false);
/** Parse the string encoding of the replace request that is found in
* \c ev.argument and act on it.
diff --git a/status.25x b/status.25x
index 708f54886a..5d8ad627ae 100644
--- a/status.25x
+++ b/status.25x
@@ -35,6 +35,9 @@ What's new
- It is now possible in the tabular creation dialog (Inset > Table) to
insert a multi-page table (optionally with caption) directly.
+- Simple search within selection now remembers the initial selection
+ beyond the first match (bug 13293).
+
* DOCUMENTATION AND LOCALIZATION
More information about the lyx-cvs
mailing list