commit 5980afaee0fe2909ddd369a35f1a81fd8a744015 Author: Juergen Spitzmueller <sp...@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. -- lyx-cvs mailing list lyx-cvs@lists.lyx.org http://lists.lyx.org/mailman/listinfo/lyx-cvs