[LyX/master] Add support for search/replace within selection (#2548)
Juergen Spitzmueller
spitz at lyx.org
Tue Feb 16 07:08:20 UTC 2021
commit 5980afaee0fe2909ddd369a35f1a81fd8a744015
Author: Juergen Spitzmueller <spitz at lyx.org>
Date: Tue Feb 16 08:11:09 2021 +0100
Add support for search/replace within selection (#2548)
---
src/BufferView.cpp | 5 +-
src/frontends/qt/GuiSearch.cpp | 17 ++--
src/frontends/qt/GuiSearch.h | 4 +-
src/frontends/qt/GuiSpellchecker.cpp | 8 +-
src/frontends/qt/GuiThesaurus.cpp | 3 +-
src/frontends/qt/Menus.cpp | 3 +-
src/frontends/qt/ui/SearchUi.ui | 12 +++-
src/lyxfind.cpp | 158 ++++++++++++++++++++++++++--------
src/lyxfind.h | 12 ++-
9 files changed, 163 insertions(+), 59 deletions(-)
diff --git a/src/BufferView.cpp b/src/BufferView.cpp
index 534e3bc..0657236 100644
--- a/src/BufferView.cpp
+++ b/src/BufferView.cpp
@@ -460,8 +460,9 @@ void BufferView::setSearchRequestCache(docstring const & text)
bool forward;
bool wrap;
bool instant;
+ bool onlysel;
docstring const search = string2find(text, casesensitive, matchword,
- forward, wrap, instant);
+ forward, wrap, instant, onlysel);
theClipboard().setFindBuffer(search);
}
@@ -1648,7 +1649,7 @@ void BufferView::dispatch(FuncRequest const & cmd, DispatchResult & dr)
docstring const data =
find2string(searched_string, false, false,
- act == LFUN_WORD_FIND_FORWARD, false, false);
+ act == LFUN_WORD_FIND_FORWARD, false, false, false);
bool found = lyxfind(this, FuncRequest(LFUN_WORD_FIND, data));
if (found)
dr.screenUpdate(Update::Force | Update::FitCursor);
diff --git a/src/frontends/qt/GuiSearch.cpp b/src/frontends/qt/GuiSearch.cpp
index 416b574..a66ba00 100644
--- a/src/frontends/qt/GuiSearch.cpp
+++ b/src/frontends/qt/GuiSearch.cpp
@@ -201,7 +201,7 @@ void GuiSearchWidget::findClicked(bool const backwards)
{
docstring const needle = qstring_to_ucs4(findCO->currentText());
find(needle, caseCB->isChecked(), wordsCB->isChecked(), !backwards,
- instantSearchCB->isChecked(), wrapCB->isChecked());
+ instantSearchCB->isChecked(), wrapCB->isChecked(), selectionCB->isChecked());
uniqueInsert(findCO, findCO->currentText());
if (!instantSearchCB->isChecked())
findCO->lineEdit()->selectAll();
@@ -219,7 +219,7 @@ void GuiSearchWidget::replaceClicked(bool const backwards)
docstring const needle = qstring_to_ucs4(findCO->currentText());
docstring const repl = qstring_to_ucs4(replaceCO->currentText());
replace(needle, repl, caseCB->isChecked(), wordsCB->isChecked(),
- !backwards, false, wrapCB->isChecked());
+ !backwards, false, wrapCB->isChecked(), selectionCB->isChecked());
uniqueInsert(findCO, findCO->currentText());
uniqueInsert(replaceCO, replaceCO->currentText());
}
@@ -235,7 +235,8 @@ void GuiSearchWidget::replaceallClicked()
{
replace(qstring_to_ucs4(findCO->currentText()),
qstring_to_ucs4(replaceCO->currentText()),
- caseCB->isChecked(), wordsCB->isChecked(), true, true, true);
+ caseCB->isChecked(), wordsCB->isChecked(),
+ true, true, true, selectionCB->isChecked());
uniqueInsert(findCO, findCO->currentText());
uniqueInsert(replaceCO, replaceCO->currentText());
}
@@ -243,11 +244,11 @@ void GuiSearchWidget::replaceallClicked()
void GuiSearchWidget::find(docstring const & search, bool casesensitive,
bool matchword, bool forward, bool instant,
- bool wrap)
+ bool wrap, bool onlysel)
{
docstring const sdata =
find2string(search, casesensitive, matchword,
- forward, wrap, instant);
+ forward, wrap, instant, onlysel);
dispatch(FuncRequest(LFUN_WORD_FIND, sdata));
}
@@ -255,11 +256,11 @@ void GuiSearchWidget::find(docstring const & search, bool casesensitive,
void GuiSearchWidget::replace(docstring const & search, docstring const & replace,
bool casesensitive, bool matchword,
- bool forward, bool all, bool wrap)
+ bool forward, bool all, bool wrap, bool onlysel)
{
docstring const sdata =
replace2string(replace, search, casesensitive,
- matchword, all, forward, wrap);
+ matchword, all, forward, true, wrap, onlysel);
dispatch(FuncRequest(LFUN_WORD_REPLACE, sdata));
}
@@ -269,6 +270,7 @@ void GuiSearchWidget::saveSession(QSettings & settings, QString const & session_
settings.setValue(session_key + "/words", wordsCB->isChecked());
settings.setValue(session_key + "/instant", instantSearchCB->isChecked());
settings.setValue(session_key + "/wrap", wrapCB->isChecked());
+ settings.setValue(session_key + "/selection", selectionCB->isChecked());
settings.setValue(session_key + "/minimized", minimized_);
}
@@ -280,6 +282,7 @@ void GuiSearchWidget::restoreSession(QString const & session_key)
wordsCB->setChecked(settings.value(session_key + "/words", false).toBool());
instantSearchCB->setChecked(settings.value(session_key + "/instant", false).toBool());
wrapCB->setChecked(settings.value(session_key + "/wrap", false).toBool());
+ selectionCB->setChecked(settings.value(session_key + "/selection", false).toBool());
minimized_ = settings.value(session_key + "/minimized", false).toBool();
// initialize hidings
minimizeClicked(false);
diff --git a/src/frontends/qt/GuiSearch.h b/src/frontends/qt/GuiSearch.h
index e90e3dc..2db6104 100644
--- a/src/frontends/qt/GuiSearch.h
+++ b/src/frontends/qt/GuiSearch.h
@@ -63,11 +63,11 @@ private:
/// Searches occurrence of string
void find(docstring const & search,
bool casesensitive, bool matchword,
- bool forward, bool instant, bool wrap);
+ bool forward, bool instant, bool wrap, bool onlysel);
/// Replaces occurrence of string
void replace(docstring const & search, docstring const & replace,
bool casesensitive, bool matchword,
- bool forward, bool all, bool wrap);
+ bool forward, bool all, bool wrap, bool onlysel);
///
BufferView const * bv_ = nullptr;
///
diff --git a/src/frontends/qt/GuiSpellchecker.cpp b/src/frontends/qt/GuiSpellchecker.cpp
index d36f562..3685ac5 100644
--- a/src/frontends/qt/GuiSpellchecker.cpp
+++ b/src/frontends/qt/GuiSpellchecker.cpp
@@ -467,7 +467,7 @@ void SpellcheckerWidget::on_findNextPB_clicked()
return;
docstring const textfield = qstring_to_ucs4(d->ui.wordED->text());
docstring const datastring = find2string(textfield,
- true, true, true, false, false);
+ true, true, true, false, false, false);
LYXERR(Debug::GUI, "Spellchecker: find next (" << textfield << ")");
dispatch(FuncRequest(LFUN_WORD_FIND, datastring));
d->canCheck();
@@ -487,7 +487,8 @@ void SpellcheckerWidget::on_replacePB_clicked()
false, // all words
true, // forward
false, // find next
- false); // auto-wrap
+ false, // auto-wrap
+ false); // only selection
LYXERR(Debug::GUI, "Replace (" << replacement << ")");
dispatch(FuncRequest(LFUN_WORD_REPLACE, datastring));
@@ -510,7 +511,8 @@ void SpellcheckerWidget::on_replaceAllPB_clicked()
true, // all words
true, // forward
false, // find next
- false); // auto-wrap
+ false, // auto-wrap
+ false); // only selection
LYXERR(Debug::GUI, "Replace all (" << replacement << ")");
dispatch(FuncRequest(LFUN_WORD_REPLACE, datastring));
diff --git a/src/frontends/qt/GuiThesaurus.cpp b/src/frontends/qt/GuiThesaurus.cpp
index d498236..795b353 100644
--- a/src/frontends/qt/GuiThesaurus.cpp
+++ b/src/frontends/qt/GuiThesaurus.cpp
@@ -267,7 +267,8 @@ void GuiThesaurus::replace(docstring const & newstr)
true, // match word
false, // all words
true, // forward
- false);// auto-wrap
+ false, // auto-wrap
+ false); // only selection
dispatch(FuncRequest(LFUN_WORD_REPLACE, sdata));
}
diff --git a/src/frontends/qt/Menus.cpp b/src/frontends/qt/Menus.cpp
index 6d77c2e..8139ff9 100644
--- a/src/frontends/qt/Menus.cpp
+++ b/src/frontends/qt/Menus.cpp
@@ -848,7 +848,8 @@ void MenuDefinition::expandSpellingSuggestions(BufferView const * bv)
false, // all words
true, // forward
false, // find next
- false))); // auto-wrap
+ false, // auto-wrap
+ false))); // only selection
if (i < m)
add(w);
else
diff --git a/src/frontends/qt/ui/SearchUi.ui b/src/frontends/qt/ui/SearchUi.ui
index aabc1c5..a1826d9 100644
--- a/src/frontends/qt/ui/SearchUi.ui
+++ b/src/frontends/qt/ui/SearchUi.ui
@@ -213,12 +213,22 @@
</widget>
</item>
<item>
+ <widget class="QCheckBox" name="selectionCB">
+ <property name="toolTip">
+ <string>Onlysearch in selection</string>
+ </property>
+ <property name="text">
+ <string>Selection onl&y</string>
+ </property>
+ </widget>
+ </item>
+ <item>
<widget class="QCheckBox" name="instantSearchCB">
<property name="toolTip">
<string>If this is checked, LyX will search forward immediately</string>
</property>
<property name="text">
- <string>Search as you t&ype</string>
+ <string>Search as yo&u type</string>
</property>
</widget>
</item>
diff --git a/src/lyxfind.cpp b/src/lyxfind.cpp
index 8487c01..6caa7fa 100644
--- a/src/lyxfind.cpp
+++ b/src/lyxfind.cpp
@@ -225,24 +225,33 @@ private:
};
-int findForward(DocIterator & cur, MatchString const & match,
- bool find_del = true)
+int findForward(DocIterator & cur, DocIterator const endcur,
+ MatchString const & match,
+ bool find_del = true, bool onlysel = false)
{
- for (; cur; cur.forwardChar())
+ for (; cur; cur.forwardChar()) {
+ if (onlysel && 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);
if (len > 0)
return len;
}
+ }
return 0;
}
-int findBackwards(DocIterator & cur, MatchString const & match,
- bool find_del = true)
+int findBackwards(DocIterator & cur, DocIterator const endcur,
+ MatchString const & match,
+ bool find_del = true, bool onlysel = false)
{
while (cur) {
cur.backwardChar();
+ if (onlysel && 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);
if (len > 0)
@@ -268,23 +277,56 @@ bool searchAllowed(docstring const & str)
bool findOne(BufferView * bv, docstring const & searchstr,
bool case_sens, bool whole, bool forward,
bool find_del, bool check_wrap, bool auto_wrap,
- bool instant)
+ bool instant, bool onlysel)
{
if (!searchAllowed(searchstr))
return false;
+ DocIterator const endcur = forward ? bv->cursor().selectionEnd() : bv->cursor().selectionBegin();
+
+ if (onlysel && bv->cursor().selection()) {
+ docstring const matchstring = bv->cursor().selectionAsString(false);
+ docstring const lcmatchsting = support::lowercase(matchstring);
+ if (matchstring == searchstr || (!case_sens && lcmatchsting == lowercase(searchstr))) {
+ docstring q = _("The search string matches the selection, and search is limited to 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();
+ if (findOne(bv, searchstr, case_sens, whole, forward,
+ find_del, check_wrap, auto_wrap, false, false))
+ return true;
+ }
+ return false;
+ }
+ }
+
DocIterator cur = forward
- ? (instant ? bv->cursor().selectionBegin() : bv->cursor().selectionEnd())
- : (instant ? bv->cursor().selectionEnd() : bv->cursor().selectionBegin());
+ ? ((instant || onlysel) ? bv->cursor().selectionBegin() : bv->cursor().selectionEnd())
+ : ((instant || onlysel) ? bv->cursor().selectionEnd() : bv->cursor().selectionBegin());
MatchString const match(searchstr, case_sens, whole);
int match_len = forward
- ? findForward(cur, match, find_del)
- : findBackwards(cur, match, find_del);
+ ? findForward(cur, endcur, match, find_del, onlysel)
+ : findBackwards(cur, endcur, match, find_del, onlysel);
if (match_len > 0)
bv->putSelectionAt(cur, match_len, !forward);
+ 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();
+ if (findOne(bv, searchstr, case_sens, whole, forward,
+ find_del, check_wrap, auto_wrap, false, false))
+ return true;
+ }
+ return false;
+ }
else if (check_wrap) {
DocIterator cur_orig(bv->cursor());
if (!auto_wrap) {
@@ -309,7 +351,7 @@ bool findOne(BufferView * bv, docstring const & searchstr,
}
bv->clearSelection();
if (findOne(bv, searchstr, case_sens, whole, forward,
- find_del, false, false, false))
+ find_del, false, false, false, false))
return true;
}
bv->cursor().setCursor(cur_orig);
@@ -324,14 +366,16 @@ namespace {
int replaceAll(BufferView * bv,
docstring const & searchstr, docstring const & replacestr,
- bool case_sens, bool whole)
+ bool case_sens, bool whole, bool onlysel)
{
Buffer & buf = bv->buffer();
if (!searchAllowed(searchstr) || buf.isReadonly())
return 0;
- DocIterator cur_orig(bv->cursor());
+ DocIterator startcur = bv->cursor().selectionBegin();
+ DocIterator endcur = bv->cursor().selectionEnd();
+ bool const had_selection = bv->cursor().selection();
MatchString const match(searchstr, case_sens, whole);
int num = 0;
@@ -341,29 +385,48 @@ int replaceAll(BufferView * bv,
Cursor cur(*bv);
cur.setCursor(doc_iterator_begin(&buf));
- int match_len = findForward(cur, match, false);
+ int match_len = findForward(cur, endcur, match, false, onlysel);
while (match_len > 0) {
// Backup current cursor position and font.
pos_type const pos = cur.pos();
Font const font = cur.paragraph().getFontSettings(buf.params(), pos);
cur.recordUndo();
- int striked = ssize -
+ int struck = ssize -
cur.paragraph().eraseChars(pos, pos + match_len,
buf.params().track_changes);
cur.paragraph().insert(pos, replacestr, font,
Change(buf.params().track_changes
? Change::INSERTED
: Change::UNCHANGED));
- for (int i = 0; i < rsize + striked; ++i)
+ for (int i = 0; i < rsize + struck; ++i)
cur.forwardChar();
+ if (onlysel && cur.pit() == endcur.pit() && cur.idx() == endcur.idx()) {
+ // Adjust end of selection for replace-all in selection
+ if (rsize > ssize) {
+ int const offset = rsize - ssize;
+ for (int i = 0; i < offset + struck; ++i)
+ endcur.forwardPos();
+ } else {
+ int const offset = ssize - rsize;
+ for (int i = 0; i < offset + struck; ++i)
+ endcur.backwardPos();
+ }
+ }
++num;
- match_len = findForward(cur, match, false);
+ match_len = findForward(cur, endcur, match, false, onlysel);
}
bv->putSelectionAt(doc_iterator_begin(&buf), 0, false);
- cur_orig.fixIfBroken();
- bv->setCursor(cur_orig);
+ startcur.fixIfBroken();
+ bv->setCursor(startcur);
+
+ // Reset selection, accounting for changes in selection
+ if (had_selection) {
+ endcur.fixIfBroken();
+ bv->cursor().resetAnchor();
+ bv->setCursorSelectionTo(endcur);
+ }
return num;
}
@@ -386,14 +449,15 @@ int replaceAll(BufferView * bv,
// whether anything at all was done.
pair<bool, int> replaceOne(BufferView * bv, docstring searchstr,
docstring const & replacestr, bool case_sens,
- bool whole, bool forward, bool findnext, bool wrap)
+ bool whole, bool forward, bool findnext, bool wrap,
+ bool onlysel)
{
Cursor & cur = bv->cursor();
- if (!cur.selection()) {
+ if (!cur.selection() || onlysel) {
// no selection, non-empty search string: find it
if (!searchstr.empty()) {
bool const found = findOne(bv, searchstr, case_sens, whole,
- forward, true, findnext, wrap, false);
+ forward, true, findnext, wrap, false, onlysel);
return make_pair(found, 0);
}
// empty search string
@@ -423,7 +487,7 @@ pair<bool, int> replaceOne(BufferView * bv, docstring searchstr,
// just find the search word
if (!have_selection || !match) {
bool const found = findOne(bv, searchstr, case_sens, whole, forward,
- true, findnext, wrap, false);
+ true, findnext, wrap, false, onlysel);
return make_pair(found, 0);
}
@@ -440,7 +504,7 @@ pair<bool, int> replaceOne(BufferView * bv, docstring searchstr,
}
if (findnext)
findOne(bv, searchstr, case_sens, whole,
- forward, false, findnext, wrap, false);
+ forward, false, findnext, wrap, false, onlysel);
return make_pair(true, 1);
}
@@ -450,7 +514,8 @@ pair<bool, int> replaceOne(BufferView * bv, docstring searchstr,
docstring const find2string(docstring const & search,
bool casesensitive, bool matchword,
- bool forward, bool wrap, bool instant)
+ bool forward, bool wrap, bool instant,
+ bool onlysel)
{
odocstringstream ss;
ss << search << '\n'
@@ -458,7 +523,8 @@ docstring const find2string(docstring const & search,
<< int(matchword) << ' '
<< int(forward) << ' '
<< int(wrap) << ' '
- << int(instant);
+ << int(instant) << ' '
+ << int(onlysel);
return ss.str();
}
@@ -466,7 +532,8 @@ docstring const find2string(docstring const & search,
docstring const replace2string(docstring const & replace,
docstring const & search,
bool casesensitive, bool matchword,
- bool all, bool forward, bool findnext, bool wrap)
+ bool all, bool forward, bool findnext,
+ bool wrap, bool onlysel)
{
odocstringstream ss;
ss << replace << '\n'
@@ -476,7 +543,8 @@ docstring const replace2string(docstring const & replace,
<< int(all) << ' '
<< int(forward) << ' '
<< int(findnext) << ' '
- << int(wrap);
+ << int(wrap) << ' '
+ << int(onlysel);
return ss.str();
}
@@ -486,11 +554,12 @@ docstring const string2find(docstring const & argument,
bool &matchword,
bool &forward,
bool &wrap,
- bool &instant)
+ bool &instant,
+ bool &onlysel)
{
// data is of the form
// "<search>
- // <casesensitive> <matchword> <forward> <wrap>"
+ // <casesensitive> <matchword> <forward> <wrap> <onlysel>"
docstring search;
docstring howto = split(argument, search, '\n');
@@ -499,6 +568,7 @@ docstring const string2find(docstring const & argument,
forward = parse_bool(howto, true);
wrap = parse_bool(howto);
instant = parse_bool(howto);
+ onlysel = parse_bool(howto);
return search;
}
@@ -515,12 +585,13 @@ bool lyxfind(BufferView * bv, FuncRequest const & ev)
bool forward;
bool wrap;
bool instant;
+ bool onlysel;
docstring search = string2find(ev.argument(), casesensitive,
- matchword, forward, wrap, instant);
+ matchword, forward, wrap, instant, onlysel);
return findOne(bv, search, casesensitive, matchword, forward,
- false, true, wrap, instant);
+ false, true, wrap, instant, onlysel);
}
@@ -532,7 +603,7 @@ bool lyxreplace(BufferView * bv, FuncRequest const & ev)
// data is of the form
// "<search>
// <replace>
- // <casesensitive> <matchword> <all> <forward> <findnext> <wrap>"
+ // <casesensitive> <matchword> <all> <forward> <findnext> <wrap> <onlysel>"
docstring search;
docstring rplc;
docstring howto = split(ev.argument(), rplc, '\n');
@@ -544,16 +615,23 @@ bool lyxreplace(BufferView * bv, FuncRequest const & ev)
bool forward = parse_bool(howto, true);
bool findnext = parse_bool(howto, true);
bool wrap = parse_bool(howto);
+ bool onlysel = parse_bool(howto);
+
+ if (!bv->cursor().selection())
+ // only selection only makes sense with selection
+ onlysel = false;
bool update = false;
int replace_count = 0;
if (all) {
- replace_count = replaceAll(bv, search, rplc, casesensitive, matchword);
+ replace_count = replaceAll(bv, search, rplc, casesensitive,
+ matchword, onlysel);
update = replace_count > 0;
} else {
pair<bool, int> rv =
- replaceOne(bv, search, rplc, casesensitive, matchword, forward, findnext, wrap);
+ replaceOne(bv, search, rplc, casesensitive, matchword,
+ forward, findnext, wrap, onlysel);
update = rv.first;
replace_count = rv.second;
}
@@ -561,15 +639,19 @@ bool lyxreplace(BufferView * bv, FuncRequest const & ev)
Buffer const & buf = bv->buffer();
if (!update) {
// emit message signal.
- buf.message(_("String not found."));
+ if (onlysel)
+ buf.message(_("String not found in selection."));
+ else
+ buf.message(_("String not found."));
} else {
if (replace_count == 0) {
buf.message(_("String found."));
} else if (replace_count == 1) {
buf.message(_("String has been replaced."));
} else {
- docstring const str =
- bformat(_("%1$d strings have been replaced."), replace_count);
+ docstring const str = onlysel
+ ? bformat(_("%1$d strings have been replaced in the selection."), replace_count)
+ : bformat(_("%1$d strings have been replaced."), replace_count);
buf.message(str);
}
}
diff --git a/src/lyxfind.h b/src/lyxfind.h
index 4847549..f79ef24 100644
--- a/src/lyxfind.h
+++ b/src/lyxfind.h
@@ -36,7 +36,8 @@ docstring const string2find(docstring const & argument,
bool &matchword,
bool &forward,
bool &wrap,
- bool &instant);
+ bool &instant,
+ bool &onlysel);
/** Encode the parameters needed to find \c search as a string
* that can be dispatched to the LyX core in a FuncRequest wrapper.
@@ -46,7 +47,8 @@ docstring const find2string(docstring const & search,
bool matchword,
bool forward,
bool wrap,
- bool instant);
+ bool instant,
+ bool onlysel);
/** Encode the parameters needed to replace \c search with \c replace
* as a string that can be dispatched to the LyX core in a FuncRequest
@@ -59,7 +61,8 @@ docstring const replace2string(docstring const & replace,
bool all,
bool forward,
bool findnext = true,
- bool wrap = true);
+ bool wrap = true,
+ bool onlysel = false);
/** Parse the string encoding of the find request that is found in
* \c ev.argument and act on it.
@@ -71,7 +74,8 @@ bool lyxfind(BufferView * bv, FuncRequest const & ev);
bool findOne(BufferView * bv, docstring const & searchstr,
bool case_sens, bool whole, bool forward,
bool find_del = true, bool check_wrap = false,
- bool auto_wrap = false, bool instant = false);
+ bool auto_wrap = false, bool instant = false,
+ bool onlysel = false);
/** Parse the string encoding of the replace request that is found in
* \c ev.argument and act on it.
More information about the lyx-cvs
mailing list