X-Git-Url: https://git.lyx.org/gitweb/?a=blobdiff_plain;ds=sidebyside;f=src%2Flyxfind.cpp;h=e95f06e27fa9d4fe963124ce12475cf36eb10bb6;hb=3915c2d01a534da1e275dc159c49971cbb170a68;hp=4186fa8f1eac0be183d29fb20ce72c44ee7072d5;hpb=f625dd7ea0770f0f1151401ed90d0ef2146595d7;p=lyx.git diff --git a/src/lyxfind.cpp b/src/lyxfind.cpp index 4186fa8f1e..e95f06e27f 100644 --- a/src/lyxfind.cpp +++ b/src/lyxfind.cpp @@ -1,12 +1,13 @@ /** * \file lyxfind.cpp * This file is part of LyX, the document processor. - * Licence details can be found in the file COPYING. + * License details can be found in the file COPYING. * - * \author Lars Gullik Bjønnes + * \author Lars Gullik Bjønnes * \author John Levon - * \author Jürgen Vigna + * \author Jürgen Vigna * \author Alfredo Braunstein + * \author Tommaso Cucinotta * * Full author contact details are available in file CREDITS. */ @@ -16,33 +17,47 @@ #include "lyxfind.h" #include "Buffer.h" -#include "BufferParams.h" -#include "Cursor.h" -#include "CutAndPaste.h" +#include "BufferList.h" #include "buffer_funcs.h" +#include "BufferParams.h" #include "BufferView.h" #include "Changes.h" -#include "debug.h" +#include "Cursor.h" +#include "CutAndPaste.h" #include "FuncRequest.h" -#include "gettext.h" -#include "Text.h" +#include "OutputParams.h" +#include "output_latex.h" #include "Paragraph.h" #include "ParIterator.h" +#include "TexRow.h" +#include "Text.h" +#include "FuncRequest.h" +#include "LyXFunc.h" + +#include "mathed/InsetMath.h" +#include "mathed/InsetMathGrid.h" +#include "mathed/InsetMathHull.h" +#include "mathed/MathStream.h" #include "frontends/alert.h" #include "support/convert.h" +#include "support/debug.h" #include "support/docstream.h" +#include "support/gettext.h" #include "support/lstrings.h" +#include "support/lassert.h" -namespace lyx { +#include "frontends/Application.h" +#include "frontends/LyXView.h" -using support::compare_no_case; -using support::uppercase; -using support::split; +#include +#include -using std::advance; +using namespace std; +using namespace lyx::support; +namespace lyx { namespace { @@ -52,11 +67,11 @@ bool parse_bool(docstring & howto) return false; docstring var; howto = split(howto, var, ' '); - return (var == "1"); + return var == "1"; } -class MatchString : public std::binary_function +class MatchString : public binary_function { public: MatchString(docstring const & str, bool cs, bool mw) @@ -104,11 +119,19 @@ bool findBackwards(DocIterator & cur, MatchString const & match, } -bool findChange(DocIterator & cur) +bool findChange(DocIterator & cur, bool next) { - for (; cur; cur.forwardPos()) - if (cur.inTexted() && !cur.paragraph().isUnchanged(cur.pos())) + if (!next) + cur.backwardPos(); + for (; cur; next ? cur.forwardPos() : cur.backwardPos()) + if (cur.inTexted() && cur.paragraph().isChanged(cur.pos())) { + if (!next) + // if we search backwards, take a step forward + // to correctly set the anchor + cur.forwardPos(); return true; + } + return false; } @@ -152,19 +175,19 @@ int replaceAll(BufferView * bv, if (!searchAllowed(bv, searchstr) || buf.isReadonly()) return 0; - bv->cursor().recordUndoFullDocument(); - MatchString const match(searchstr, cs, mw); int num = 0; int const rsize = replacestr.size(); int const ssize = searchstr.size(); - DocIterator cur = doc_iterator_begin(buf.inset()); + Cursor cur(*bv); + cur.setCursor(doc_iterator_begin(&buf)); while (findForward(cur, match, false)) { - pos_type pos = cur.pos(); - Font const font - = cur.paragraph().getFontSettings(buf.params(), pos); + // 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 - cur.paragraph().eraseChars(pos, pos + ssize, buf.params().trackChanges); cur.paragraph().insert(pos, replacestr, font, @@ -175,17 +198,25 @@ int replaceAll(BufferView * bv, ++num; } - updateLabels(buf); - bv->putSelectionAt(doc_iterator_begin(buf.inset()), 0, false); + buf.updateLabels(); + bv->putSelectionAt(doc_iterator_begin(&buf), 0, false); if (num) buf.markDirty(); return num; } -bool stringSelected(BufferView * bv, docstring const & searchstr, +bool stringSelected(BufferView * bv, docstring & searchstr, bool cs, bool mw, bool fw) { + // if nothing selected and searched string is empty, this + // means that we want to search current word at cursor position. + if (!bv->cursor().selection() && searchstr.empty()) { + bv->cursor().innerText()->selectWord(bv->cursor(), WHOLE_WORD); + searchstr = bv->cursor().selectionAsString(false); + return true; + } + // if nothing selected or selection does not equal search // string search and select next occurance and return docstring const & str1 = searchstr; @@ -199,19 +230,20 @@ bool stringSelected(BufferView * bv, docstring const & searchstr, } -int replace(BufferView * bv, docstring const & searchstr, +int replace(BufferView * bv, docstring & searchstr, docstring const & replacestr, bool cs, bool mw, bool fw) { - if (!searchAllowed(bv, searchstr) || bv->buffer().isReadonly()) + if (!stringSelected(bv, searchstr, cs, mw, fw)) return 0; - if (!stringSelected(bv, searchstr, cs, mw, fw)) + if (!searchAllowed(bv, searchstr) || bv->buffer().isReadonly()) return 0; Cursor & cur = bv->cursor(); cap::replaceSelectionWithString(cur, replacestr, fw); bv->buffer().markDirty(); find(bv, searchstr, cs, mw, fw, false); + bv->buffer().updateMacros(); bv->processUpdateFlags(Update::Force | Update::FitCursor); return 1; @@ -232,13 +264,13 @@ docstring const find2string(docstring const & search, } -docstring const replace2string(docstring const & search, docstring const & replace, - bool casesensitive, bool matchword, - bool all, bool forward) +docstring const replace2string(docstring const & replace, + docstring const & search, bool casesensitive, bool matchword, + bool all, bool forward) { odocstringstream ss; - ss << search << '\n' - << replace << '\n' + ss << replace << '\n' + << search << '\n' << int(casesensitive) << ' ' << int(matchword) << ' ' << int(all) << ' ' @@ -247,12 +279,12 @@ docstring const replace2string(docstring const & search, docstring const & repla } -void find(BufferView * bv, FuncRequest const & ev) +bool find(BufferView * bv, FuncRequest const & ev) { if (!bv || ev.action != LFUN_WORD_FIND) - return; + return false; - //lyxerr << "find called, cmd: " << ev << std::endl; + //lyxerr << "find called, cmd: " << ev << endl; // data is of the form // " @@ -264,12 +296,7 @@ void find(BufferView * bv, FuncRequest const & ev) bool matchword = parse_bool(howto); bool forward = parse_bool(howto); - bool const found = find(bv, search, - casesensitive, matchword, forward); - - if (!found) - // emit message signal. - bv->message(_("String not found!")); + return find(bv, search, casesensitive, matchword, forward); } @@ -284,8 +311,8 @@ void replace(BufferView * bv, FuncRequest const & ev, bool has_deleted) // " docstring search; docstring rplc; - docstring howto = split(ev.argument(), search, '\n'); - howto = split(howto, rplc, '\n'); + docstring howto = split(ev.argument(), rplc, '\n'); + howto = split(howto, search, '\n'); bool casesensitive = parse_bool(howto); bool matchword = parse_bool(howto); @@ -296,7 +323,7 @@ void replace(BufferView * bv, FuncRequest const & ev, bool has_deleted) int const replace_count = all ? replaceAll(bv, search, rplc, casesensitive, matchword) : replace(bv, search, rplc, casesensitive, matchword, forward); - + Buffer & buf = bv->buffer(); if (replace_count == 0) { // emit message signal. @@ -315,11 +342,9 @@ void replace(BufferView * bv, FuncRequest const & ev, bool has_deleted) } else { // if we have deleted characters, we do not replace at all, but // rather search for the next occurence - bool const found = find(bv, search, - casesensitive, matchword, forward); - - if (!found) - // emit message signal. + if (find(bv, search, casesensitive, matchword, forward)) + bv->showCursor(); + else bv->message(_("String not found!")); } } @@ -327,32 +352,1082 @@ void replace(BufferView * bv, FuncRequest const & ev, bool has_deleted) bool findNextChange(BufferView * bv) { + return findChange(bv, true); +} + + +bool findPreviousChange(BufferView * bv) +{ + return findChange(bv, false); +} + + +bool findChange(BufferView * bv, bool next) +{ + if (bv->cursor().selection()) { + // set the cursor at the beginning or at the end of the selection + // before searching. Otherwise, the current change will be found. + if (next != (bv->cursor().top() > bv->cursor().anchor())) + bv->cursor().setCursorToAnchor(); + } + DocIterator cur = bv->cursor(); - if (!findChange(cur)) + // Are we within a change ? Then first search forward (backward), + // clear the selection and search the other way around (see the end + // of this function). This will avoid changes to be selected half. + bool search_both_sides = false; + if (cur.pos() > 1) { + Change change_next_pos + = cur.paragraph().lookupChange(cur.pos()); + Change change_prev_pos + = cur.paragraph().lookupChange(cur.pos() - 1); + if (change_next_pos.isSimilarTo(change_prev_pos)) + search_both_sides = true; + } + + if (!findChange(cur, next)) return false; bv->cursor().setCursor(cur); bv->cursor().resetAnchor(); + if (!next) + // take a step into the change + cur.backwardPos(); + Change orig_change = cur.paragraph().lookupChange(cur.pos()); CursorSlice & tip = cur.top(); - for (; !tip.at_end(); tip.forwardPos()) { - Change change = tip.paragraph().lookupChange(tip.pos()); - if (change != orig_change) - break; + if (next) { + for (; !tip.at_end(); tip.forwardPos()) { + Change change = tip.paragraph().lookupChange(tip.pos()); + if (change != orig_change) + break; + } + } else { + for (; !tip.at_begin();) { + tip.backwardPos(); + Change change = tip.paragraph().lookupChange(tip.pos()); + if (change != orig_change) { + // take a step forward to correctly set the selection + tip.forwardPos(); + break; + } + } } - // avoid crash (assertion violation) if the imaginary end-of-par - // character of the last paragraph of the document is marked as changed - if (tip.at_end()) - tip.backwardPos(); // Now put cursor to end of selection: bv->cursor().setCursor(cur); bv->cursor().setSelection(); + if (search_both_sides) { + bv->cursor().setSelection(false); + findChange(bv, !next); + } + + return true; +} + +namespace { + +typedef vector > Escapes; + +/// A map of symbols and their escaped equivalent needed within a regex. +Escapes const & get_regexp_escapes() +{ + static Escapes escape_map; + if (escape_map.empty()) { + escape_map.push_back(pair("\\", "\\\\")); + escape_map.push_back(pair("^", "\\^")); + escape_map.push_back(pair("$", "\\$")); + escape_map.push_back(pair("{", "\\{")); + escape_map.push_back(pair("}", "\\}")); + escape_map.push_back(pair("[", "\\[")); + escape_map.push_back(pair("]", "\\]")); + escape_map.push_back(pair("(", "\\(")); + escape_map.push_back(pair(")", "\\)")); + escape_map.push_back(pair("+", "\\+")); + escape_map.push_back(pair("*", "\\*")); + escape_map.push_back(pair(".", "\\.")); + } + return escape_map; +} + +/// A map of lyx escaped strings and their unescaped equivalent. +Escapes const & get_lyx_unescapes() { + static Escapes escape_map; + if (escape_map.empty()) { + escape_map.push_back(pair("{*}", "*")); + escape_map.push_back(pair("{[}", "[")); + escape_map.push_back(pair("\\$", "$")); + escape_map.push_back(pair("\\backslash{}", "\\")); + escape_map.push_back(pair("\\backslash", "\\")); + escape_map.push_back(pair("\\sim ", "~")); + escape_map.push_back(pair("\\^", "^")); + } + return escape_map; +} + +/** @todo Probably the maps need to be migrated to regexps, in order to distinguish if + ** the found occurrence were escaped. + **/ +string apply_escapes(string s, Escapes const & escape_map) +{ + LYXERR(Debug::FIND, "Escaping: '" << s << "'"); + Escapes::const_iterator it; + for (it = escape_map.begin(); it != escape_map.end(); ++it) { +// LYXERR(Debug::FIND, "Escaping " << it->first << " as " << it->second); + unsigned int pos = 0; + while (pos < s.length() && (pos = s.find(it->first, pos)) < s.length()) { + s.replace(pos, it->first.length(), it->second); +// LYXERR(Debug::FIND, "After escape: " << s); + pos += it->second.length(); +// LYXERR(Debug::FIND, "pos: " << pos); + } + } + LYXERR(Debug::FIND, "Escaped : '" << s << "'"); + return s; +} + +/** Return the position of the closing brace matching the open one at s[pos], + ** or s.size() if not found. + **/ +size_t find_matching_brace(string const & s, size_t pos) +{ + LASSERT(s[pos] == '{', /* */); + int open_braces = 1; + for (++pos; pos < s.size(); ++pos) { + if (s[pos] == '\\') + ++pos; + else if (s[pos] == '{') + ++open_braces; + else if (s[pos] == '}') { + --open_braces; + if (open_braces == 0) + return pos; + } + } + return s.size(); +} + +/// Within \regexp{} apply get_regex_escapes(), while outside apply get_lyx_unescapes(). +string escape_for_regex(string s) +{ + size_t pos = 0; + while (pos < s.size()) { + size_t new_pos = s.find("\\regexp{", pos); + if (new_pos == string::npos) + new_pos = s.size(); + LYXERR(Debug::FIND, "new_pos: " << new_pos); + string t = apply_escapes(s.substr(pos, new_pos - pos), get_lyx_unescapes()); + LYXERR(Debug::FIND, "t : " << t); + t = apply_escapes(t, get_regexp_escapes()); + s.replace(pos, new_pos - pos, t); + new_pos = pos + t.size(); + LYXERR(Debug::FIND, "Regexp after escaping: " << s); + LYXERR(Debug::FIND, "new_pos: " << new_pos); + if (new_pos == s.size()) + break; + size_t end_pos = find_matching_brace(s, new_pos + 7); + LYXERR(Debug::FIND, "end_pos: " << end_pos); + t = apply_escapes(s.substr(new_pos + 8, end_pos - (new_pos + 8)), get_lyx_unescapes()); + LYXERR(Debug::FIND, "t : " << t); + if (end_pos == s.size()) { + s.replace(new_pos, end_pos - new_pos, t); + pos = s.size(); + LYXERR(Debug::FIND, "Regexp after \\regexp{} removal: " << s); + break; + } + s.replace(new_pos, end_pos + 1 - new_pos, t); + LYXERR(Debug::FIND, "Regexp after \\regexp{} removal: " << s); + pos = new_pos + t.size(); + LYXERR(Debug::FIND, "pos: " << pos); + } + return s; +} + +/// Wrapper for boost::regex_replace with simpler interface +bool regex_replace(string const & s, string & t, string const & searchstr, + string const & replacestr) +{ + boost::regex e(searchstr); + ostringstream oss; + ostream_iterator it(oss); + boost::regex_replace(it, s.begin(), s.end(), e, replacestr); + // tolerate t and s be references to the same variable + bool rv = (s != oss.str()); + t = oss.str(); + return rv; +} + +/** Checks if supplied string segment is well-formed from the standpoint of matching open-closed braces. + ** + ** Verify that closed braces exactly match open braces. This avoids that, for example, + ** \frac{.*}{x} matches \frac{x+\frac{y}{x}}{z} with .* being 'x+\frac{y'. + ** + ** @param unmatched + ** Number of open braces that must remain open at the end for the verification to succeed. + **/ +bool braces_match(string::const_iterator const & beg, + string::const_iterator const & end, int unmatched = 0) +{ + int open_pars = 0; + string::const_iterator it = beg; + LYXERR(Debug::FIND, "Checking " << unmatched << " unmatched braces in '" << string(beg, end) << "'"); + for (; it != end; ++it) { + // Skip escaped braces in the count + if (*it == '\\') { + ++it; + if (it == end) + break; + } else if (*it == '{') { + ++open_pars; + } else if (*it == '}') { + if (open_pars == 0) { + LYXERR(Debug::FIND, "Found unmatched closed brace"); + return false; + } else + --open_pars; + } + } + if (open_pars != unmatched) { + LYXERR(Debug::FIND, "Found " << open_pars + << " instead of " << unmatched + << " unmatched open braces at the end of count"); + return false; + } + LYXERR(Debug::FIND, "Braces match as expected"); return true; } +/** The class performing a match between a position in the document and the FindAdvOptions. + **/ +class MatchStringAdv { +public: + MatchStringAdv(lyx::Buffer & buf, FindAndReplaceOptions const & opt); + + /** Tests if text starting at the supplied position matches with the one provided to the MatchStringAdv + ** constructor as opt.search, under the opt.* options settings. + ** + ** @param at_begin + ** If set, then match is searched only against beginning of text starting at cur. + ** If unset, then match is searched anywhere in text starting at cur. + ** + ** @return + ** The length of the matching text, or zero if no match was found. + **/ + int operator()(DocIterator const & cur, int len = -1, bool at_begin = true) const; + +public: + /// buffer + lyx::Buffer * p_buf; + /// first buffer on which search was started + lyx::Buffer * const p_first_buf; + /// options + FindAndReplaceOptions const & opt; + +private: + /** Normalize a stringified or latexified LyX paragraph. + ** + ** Normalize means: + **
    + **
  • if search is not casesensitive, then lowercase the string; + **
  • remove any newline at begin or end of the string; + **
  • replace any newline in the middle of the string with a simple space; + **
  • remove stale empty styles and environments, like \emph{} and \textbf{}. + **
+ ** + ** @todo Normalization should also expand macros, if the corresponding + ** search option was checked. + **/ + string normalize(docstring const & s) const; + // normalized string to search + string par_as_string; + // regular expression to use for searching + boost::regex regexp; + // same as regexp, but prefixed with a ".*" + boost::regex regexp2; + // unmatched open braces in the search string/regexp + int open_braces; + // number of (.*?) subexpressions added at end of search regexp for closing + // environments, math mode, styles, etc... + int close_wildcards; +}; + + +MatchStringAdv::MatchStringAdv(lyx::Buffer & buf, FindAndReplaceOptions const & opt) + : p_buf(&buf), p_first_buf(&buf), opt(opt) +{ + par_as_string = normalize(opt.search); + open_braces = 0; + close_wildcards = 0; + + if (! opt.regexp) { + // Remove trailing closure of math, macros and environments, so to catch parts of them. + do { + LYXERR(Debug::FIND, "par_as_string now is '" << par_as_string << "'"); + if (regex_replace(par_as_string, par_as_string, "(.*)[[:blank:]]\\'", "$1")) + continue; + if (regex_replace(par_as_string, par_as_string, "(.*[^\\\\]) ?\\$\\'", "$1")) + continue; + // @todo need to account for open square braces as well ? + if (regex_replace(par_as_string, par_as_string, "(.*[^\\\\]) ?\\\\\\]\\'", "$1")) + continue; + if (regex_replace(par_as_string, par_as_string, "(.*[^\\\\]) ?\\\\end\\{[a-zA-Z_]*\\}\\'", "$1")) + continue; + if (regex_replace(par_as_string, par_as_string, "(.*[^\\\\]) ?\\}\\'", "$1")) { + ++open_braces; + continue; + } + break; + } while (true); + LYXERR(Debug::FIND, "Open braces: " << open_braces); + LASSERT(braces_match(par_as_string.begin(), par_as_string.end(), open_braces), /* */); + LYXERR(Debug::FIND, "Built MatchStringAdv object: par_as_string = '" << par_as_string << "'"); + } else { + par_as_string = escape_for_regex(par_as_string); + // Insert (.*?) before trailing closure of math, macros and environments, so to catch parts of them. + LYXERR(Debug::FIND, "par_as_string now is '" << par_as_string << "'"); + if ( + // Insert .* before trailing '\$' ('$' has been escaped by escape_for_regex) + regex_replace(par_as_string, par_as_string, "(.*[^\\\\])(\\\\\\$)\\'", "$1(.*?)$2") + // Insert .* before trailing '\\\]' ('\]' has been escaped by escape_for_regex) + || regex_replace(par_as_string, par_as_string, "(.*[^\\\\])(\\\\\\\\\\\\\\])\\'", "$1(.*?)$2") + // Insert .* before trailing '\\end\{...}' ('\end{...}' has been escaped by escape_for_regex) + || regex_replace(par_as_string, par_as_string, + "(.*[^\\\\])(\\\\\\\\end\\\\\\{[a-zA-Z_]*\\\\\\})\\'", "$1(.*?)$2") + // Insert .* before trailing '\}' ('}' has been escaped by escape_for_regex) + || regex_replace(par_as_string, par_as_string, "(.*[^\\\\])(\\\\\\})\\'", "$1(.*?)$2") + ) { + ++close_wildcards; + } + LYXERR(Debug::FIND, "par_as_string now is '" << par_as_string << "'"); + LYXERR(Debug::FIND, "Open braces: " << open_braces); + LYXERR(Debug::FIND, "Close .*? : " << close_wildcards); + LASSERT(braces_match(par_as_string.begin(), par_as_string.end(), open_braces), /* */); + LYXERR(Debug::FIND, "Replaced text (to be used as regex): " << par_as_string); + // If entered regexp must match at begin of searched string buffer + regexp = boost::regex(string("\\`") + par_as_string); + // If entered regexp may match wherever in searched string buffer + regexp2 = boost::regex(string("\\`.*") + par_as_string); + } +} + + +int MatchStringAdv::operator()(DocIterator const & cur, int len, bool at_begin) const +{ + docstring docstr = stringifyFromForSearch(opt, cur, len); + LYXERR(Debug::FIND, "Matching against '" << lyx::to_utf8(docstr) << "'"); + string str = normalize(docstr); + LYXERR(Debug::FIND, "After normalization: '" << str << "'"); + if (! opt.regexp) { + if (at_begin) { + if (str.substr(0, par_as_string.size()) == par_as_string) + return par_as_string.size(); + } else { + size_t pos = str.find(par_as_string); + if (pos != string::npos) + return par_as_string.size(); + } + } else { + // Try all possible regexp matches, + //until one that verifies the braces match test is found + boost::regex const *p_regexp = at_begin ? ®exp : ®exp2; + boost::sregex_iterator re_it(str.begin(), str.end(), *p_regexp); + boost::sregex_iterator re_it_end; + for (; re_it != re_it_end; ++re_it) { + boost::match_results const & m = *re_it; + // Check braces on the segment that matched the entire regexp expression, + // plus the last subexpression, if a (.*?) was inserted in the constructor. + if (! braces_match(m[0].first, m[0].second, open_braces)) + return 0; + // Check braces on segments that matched all (.*?) subexpressions. + for (size_t i = 1; i < m.size(); ++i) + if (! braces_match(m[i].first, m[i].second)) + return false; + // Exclude from the returned match length any length + // due to close wildcards added at end of regexp + if (close_wildcards == 0) + return m[0].second - m[0].first; + else + return m[m.size() - close_wildcards].first - m[0].first; + } + } + return 0; +} + + +string MatchStringAdv::normalize(docstring const & s) const +{ + string t; + if (! opt.casesensitive) + t = lyx::to_utf8(lowercase(s)); + else + t = lyx::to_utf8(s); + // Remove \n at begin + while (t.size() > 0 && t[0] == '\n') + t = t.substr(1); + // Remove \n at end + while (t.size() > 0 && t[t.size() - 1] == '\n') + t = t.substr(0, t.size() - 1); + size_t pos; + // Replace all other \n with spaces + while ((pos = t.find("\n")) != string::npos) + t.replace(pos, 1, " "); + // Remove stale empty \emph{}, \textbf{} and similar blocks from latexify + LYXERR(Debug::FIND, "Removing stale empty \\emph{}, \\textbf{}, \\*section{} macros from: " << t); + while (regex_replace(t, t, "\\\\(emph|textbf|subsubsection|subsection|section|subparagraph|paragraph)(\\{\\})+", "")) + LYXERR(Debug::FIND, " further removing stale empty \\emph{}, \\textbf{} macros from: " << t); + return t; +} + + +docstring stringifyFromCursor(DocIterator const & cur, int len) +{ + LYXERR(Debug::FIND, "Stringifying with len=" << len << " from cursor at pos: " << cur); + if (cur.inTexted()) { + Paragraph const & par = cur.paragraph(); + // TODO what about searching beyond/across paragraph breaks ? + // TODO Try adding a AS_STR_INSERTS as last arg + pos_type end = ( len == -1 || cur.pos() + len > int(par.size()) ) ? + int(par.size()) : cur.pos() + len; + OutputParams runparams(&cur.buffer()->params().encoding()); + odocstringstream os; + runparams.nice = true; + runparams.flavor = OutputParams::LATEX; + runparams.linelen = 100000; //lyxrc.plaintext_linelen; + // No side effect of file copying and image conversion + runparams.dryrun = true; + LYXERR(Debug::FIND, "Stringifying with cur: " + << cur << ", from pos: " << cur.pos() << ", end: " << end); + return par.stringify(cur.pos(), end, AS_STR_INSETS, runparams); + } else if (cur.inMathed()) { + odocstringstream os; + CursorSlice cs = cur.top(); + MathData md = cs.cell(); + MathData::const_iterator it_end = + ( ( len == -1 || cs.pos() + len > int(md.size()) ) + ? md.end() : md.begin() + cs.pos() + len ); + for (MathData::const_iterator it = md.begin() + cs.pos(); it != it_end; ++it) + os << *it; + return os.str(); + } + LYXERR(Debug::FIND, "Don't know how to stringify from here: " << cur); + return docstring(); +} + + +/** Computes the LaTeX export of buf starting from cur and ending len positions + * after cur, if len is positive, or at the paragraph or innermost inset end + * if len is -1. + */ +docstring latexifyFromCursor(DocIterator const & cur, int len) +{ + LYXERR(Debug::FIND, "Latexifying with len=" << len << " from cursor at pos: " << cur); + LYXERR(Debug::FIND, " with cur.lastpost=" << cur.lastpos() << ", cur.lastrow=" + << cur.lastrow() << ", cur.lastcol=" << cur.lastcol()); + Buffer const & buf = *cur.buffer(); + LASSERT(buf.isLatex(), /* */); + + TexRow texrow; + odocstringstream ods; + OutputParams runparams(&buf.params().encoding()); + runparams.nice = false; + runparams.flavor = OutputParams::LATEX; + runparams.linelen = 8000; //lyxrc.plaintext_linelen; + // No side effect of file copying and image conversion + runparams.dryrun = true; + + if (cur.inTexted()) { + // @TODO what about searching beyond/across paragraph breaks ? + ParagraphList::const_iterator pit = cur.innerText()->paragraphs().begin(); + for (int i = 0; i < cur.pit(); ++i) + ++pit; +// ParagraphList::const_iterator pit_end = pit; +// ++pit_end; +// lyx::latexParagraphs(buf, cur.innerText()->paragraphs(), ods, texrow, runparams, string(), pit, pit_end); + pos_type const endpos = (len == -1 || cur.pos() + len > int(pit->size())) + ? pit->size() : cur.pos() + len; + TeXOnePar(buf, *cur.innerText(), pit, ods, texrow, runparams, string(), + cur.pos(), endpos); + LYXERR(Debug::FIND, "Latexified text: '" << lyx::to_utf8(ods.str()) << "'"); + } else if (cur.inMathed()) { + // Retrieve the math environment type, and add '$' or '$[' or others (\begin{equation}) accordingly + for (int s = cur.depth() - 1; s >= 0; --s) { + CursorSlice const & cs = cur[s]; + if (cs.asInsetMath() && cs.asInsetMath() && cs.asInsetMath()->asHullInset()) { + WriteStream ws(ods); + cs.asInsetMath()->asHullInset()->header_write(ws); + break; + } + } + + CursorSlice const & cs = cur.top(); + MathData md = cs.cell(); + MathData::const_iterator it_end = ( ( len == -1 || cs.pos() + len > int(md.size()) ) + ? md.end() : md.begin() + cs.pos() + len ); + for (MathData::const_iterator it = md.begin() + cs.pos(); it != it_end; ++it) + ods << *it; + + // MathData md = cur.cell(); + // MathData::const_iterator it_end = ( ( len == -1 || cur.pos() + len > int(md.size()) ) ? md.end() : md.begin() + cur.pos() + len ); + // for (MathData::const_iterator it = md.begin() + cur.pos(); it != it_end; ++it) { + // MathAtom const & ma = *it; + // ma.nucleus()->latex(buf, ods, runparams); + // } + + // Retrieve the math environment type, and add '$' or '$]' + // or others (\end{equation}) accordingly + for (int s = cur.depth() - 1; s >= 0; --s) { + CursorSlice const & cs = cur[s]; + InsetMath * inset = cs.asInsetMath(); + if (inset && inset->asHullInset()) { + WriteStream ws(ods); + inset->asHullInset()->footer_write(ws); + break; + } + } + LYXERR(Debug::FIND, "Latexified math: '" << lyx::to_utf8(ods.str()) << "'"); + } else { + LYXERR(Debug::FIND, "Don't know how to stringify from here: " << cur); + } + return ods.str(); +} + + +/** Finalize an advanced find operation, advancing the cursor to the innermost + ** position that matches, plus computing the length of the matching text to + ** be selected + **/ +int findAdvFinalize(DocIterator & cur, MatchStringAdv const & match) +{ + // Search the foremost position that matches (avoids find of entire math + // inset when match at start of it) + size_t d; + DocIterator old_cur(cur.buffer()); + do { + LYXERR(Debug::FIND, "Forwarding one step (searching for innermost match)"); + d = cur.depth(); + old_cur = cur; + cur.forwardPos(); + } while (cur && cur.depth() > d && match(cur) > 0); + cur = old_cur; + LASSERT(match(cur) > 0, /* */); + LYXERR(Debug::FIND, "Ok"); + + // Compute the match length + int len = 1; + LYXERR(Debug::FIND, "verifying unmatch with len = " << len); + while (cur.pos() + len <= cur.lastpos() && match(cur, len) == 0) { + ++len; + LYXERR(Debug::FIND, "verifying unmatch with len = " << len); + } + // Length of matched text (different from len param) + int old_len = match(cur, len); + int new_len; + // Greedy behaviour while matching regexps + while ((new_len = match(cur, len + 1)) > old_len) { + ++len; + old_len = new_len; + LYXERR(Debug::FIND, "verifying match with len = " << len); + } + return len; +} + + +/** Switch p_buf to point to next document buffer. + ** + ** Return true if restarted from master-document buffer. + ** + ** @note + ** Not using p_buf->allRelatives() here, because I'm not sure + ** whether or not the returned order is independent of p_buf. + **/ +bool next_document_buffer(Buffer * & p_buf) { + Buffer *p_master = p_buf; + Buffer *p_old; + do { + p_old = p_master; + p_master = const_cast(p_master->masterBuffer()); + LYXERR(Debug::FIND, "p_old=" << p_old << ", p_master=" << p_master); + } while (p_master != p_old); + LASSERT(p_master != NULL, /**/); + vector v_children; + /* Root master added as first buffer in the vector */ + v_children.push_back(p_master); + p_master->getChildren(v_children, true); + LYXERR(Debug::FIND, "v_children.size()=" << v_children.size()); + vector::const_iterator it = find(v_children.begin(), v_children.end(), p_buf); + LASSERT(it != v_children.end(), /**/) + ++it; + if (it == v_children.end()) { + p_buf = *v_children.begin(); + return true; + } + p_buf = *it; + return false; +} + + +/** Switch p_buf to point to previous document buffer. + ** + ** Return true if restarted from last child buffer. + ** + ** @note + ** Not using p_buf->allRelatives() here, because I'm not sure + ** whether or not the returned order is independent of p_buf. + **/ +bool prev_document_buffer(Buffer * & p_buf) { + Buffer *p_master = p_buf; + Buffer *p_old; + do { + p_old = p_master; + p_master = const_cast(p_master->masterBuffer()); + LYXERR(Debug::FIND, "p_old=" << p_old << ", p_master=" << p_master); + } while (p_master != p_old); + LASSERT(p_master != NULL, /**/); + vector v_children; + /* Root master added as first buffer in the vector */ + v_children.push_back(p_master); + p_master->getChildren(v_children, true); + LYXERR(Debug::FIND, "v_children.size()=" << v_children.size()); + vector::const_iterator it = find(v_children.begin(), v_children.end(), p_buf); + LASSERT(it != v_children.end(), /**/) + if (it == v_children.begin()) { + it = v_children.end(); + --it; + p_buf = *it; + return true; + } + --it; + p_buf = *it; + return false; +} + + +/** Switch p_buf to point to next open buffer. + ** + ** Return true if restarted from first open buffer. + **/ +bool next_open_buffer(Buffer * & p_buf) { + p_buf = theBufferList().next(p_buf); + return p_buf == *theBufferList().begin(); +} + + +/** Switch p_buf to point to previous open buffer. + ** + ** Return true if restarted from last open buffer. + **/ +bool prev_open_buffer(Buffer * & p_buf) { + p_buf = theBufferList().previous(p_buf); + return p_buf == *(theBufferList().end() - 1); +} + + +/// Finds forward +int findForwardAdv(DocIterator & cur, MatchStringAdv & match) +{ + if (!cur) + return 0; + int wrap_answer = -1; + do { + while (cur && !match(cur, -1, false)) { + if (cur.pit() < cur.lastpit()) + cur.forwardPar(); + else { + cur.forwardPos(); + } + } + for (; cur; cur.forwardPos()) { + if (match(cur)) + return findAdvFinalize(cur, match); + } + // No match has been found in current buffer + bool prompt = false; + switch (match.opt.scope) { + case FindAndReplaceOptions::S_BUFFER: + prompt = true; + break; + case FindAndReplaceOptions::S_DOCUMENT: + prompt = next_document_buffer(match.p_buf); + break; + case FindAndReplaceOptions::S_OPEN_BUFFERS: + prompt = next_open_buffer(match.p_buf); + break; + } + if (prompt) { + if (wrap_answer != -1) + break; + wrap_answer = frontend::Alert::prompt( + _("Wrap search?"), + _("End of document/scope reached while searching forward.\n" + "\n" + "Continue searching from beginning?"), + 0, 1, _("&Yes"), _("&No")); + } + cur.clear(); + cur.push_back(CursorSlice(match.p_buf->inset())); + } while (wrap_answer != 1); + return 0; +} + + +/// Find the most backward consecutive match within same paragraph while searching backwards. +void findMostBackwards(DocIterator & cur, MatchStringAdv const & match, int & len) { + DocIterator cur_begin = doc_iterator_begin(cur.buffer()); + len = findAdvFinalize(cur, match); + if (cur != cur_begin) { + Inset & inset = cur.inset(); + int old_len; + DocIterator old_cur; + DocIterator dit2; + do { + old_cur = cur; + old_len = len; + cur.backwardPos(); + LYXERR(Debug::FIND, "findMostBackwards(): old_cur=" + << old_cur << ", old_len=" << len << ", cur=" << cur); + dit2 = cur; + } while (cur != cur_begin && &cur.inset() == &inset && match(cur) + && (len = findAdvFinalize(dit2, match)) > old_len); + cur = old_cur; + len = old_len; + } + LYXERR(Debug::FIND, "findMostBackwards(): cur=" << cur); +} + +/// Finds backwards +int findBackwardsAdv(DocIterator & cur, MatchStringAdv & match) { + if (! cur) + return 0; + // Backup of original position (for restoring it in case match not found) + DocIterator cur_orig(cur); + // Position beyond which match is not considered + // (set to end of document after wrap-around question) + DocIterator cur_orig2(cur); + DocIterator cur_begin = doc_iterator_begin(cur.buffer()); +/* if (match(cur_orig)) */ +/* findAdvFinalize(cur_orig, match); */ + int wrap_answer = 0; + bool found_match; + do { + bool pit_changed = false; + found_match = false; + // Search in current par occurs from start to end, + // but in next loop match is discarded if pos > original pos + cur.pos() = 0; + found_match = match(cur, -1, false); + LYXERR(Debug::FIND, "findBackAdv0: found_match=" << found_match << ", cur: " << cur); + while (cur != cur_begin) { + if (found_match) + break; + if (cur.pit() > 0) + --cur.pit(); + else + cur.backwardPos(); + pit_changed = true; + // Search in previous pars occurs from start to end + cur.pos() = 0; + found_match = match(cur, -1, false); + LYXERR(Debug::FIND, "findBackAdv1: found_match=" + << found_match << ", cur: " << cur); + } + if (pit_changed) + cur.pos() = cur.lastpos(); + else + cur.pos() = cur_orig2.pos(); + LYXERR(Debug::FIND, "findBackAdv2: cur: " << cur); + DocIterator cur_prev_iter; + if (found_match) { + while (true) { + found_match=match(cur); + LYXERR(Debug::FIND, "findBackAdv3: found_match=" + << found_match << ", cur: " << cur); + if (found_match) { + int len; + findMostBackwards(cur, match, len); + if (&cur.inset() != &cur_orig2.inset() + || !(cur.pit() == cur_orig2.pit()) + || cur.pos() < cur_orig2.pos()) + return len; + } + // Prevent infinite loop at begin of document + if (cur == cur_begin || cur == cur_prev_iter) + break; + cur_prev_iter = cur; + cur.backwardPos(); + }; + } + // No match has been found in current buffer + bool prompt = false; + switch (match.opt.scope) { + case FindAndReplaceOptions::S_BUFFER: + prompt = true; + break; + case FindAndReplaceOptions::S_DOCUMENT: + prompt = prev_document_buffer(match.p_buf); + break; + case FindAndReplaceOptions::S_OPEN_BUFFERS: + prompt = prev_open_buffer(match.p_buf); + break; + } + if (prompt) { + wrap_answer = frontend::Alert::prompt( + _("Wrap search?"), + _("Beginning of document/scope reached while searching backwards\n" + "\n" + "Continue searching from end?"), + 0, 1, _("&Yes"), _("&No")); + } + cur = doc_iterator_end(match.p_buf); + cur.backwardPos(); + LYXERR(Debug::FIND, "findBackAdv5: cur: " << cur); + cur_orig2 = cur; + } while (wrap_answer == 0); + cur = cur_orig; + return 0; +} + + +} // anonym namespace + + +docstring stringifyFromForSearch(FindAndReplaceOptions const & opt, + DocIterator const & cur, int len) +{ + if (!opt.ignoreformat) + return latexifyFromCursor(cur, len); + else + return stringifyFromCursor(cur, len); +} + + +FindAndReplaceOptions::FindAndReplaceOptions(docstring const & search, bool casesensitive, + bool matchword, bool forward, bool expandmacros, bool ignoreformat, + bool regexp, docstring const & replace, bool keep_case, + SearchScope scope) + : search(search), casesensitive(casesensitive), matchword(matchword), + forward(forward), expandmacros(expandmacros), ignoreformat(ignoreformat), + regexp(regexp), replace(replace), keep_case(keep_case), scope(scope) +{ +} + + +/** Checks if the supplied character is lower-case */ +static bool isLowerCase(char_type ch) { + return lowercase(ch) == ch; +} + + +/** Checks if the supplied character is upper-case */ +static bool isUpperCase(char_type ch) { + return uppercase(ch) == ch; +} + + +/** Check if 'len' letters following cursor are all non-lowercase */ +static bool allNonLowercase(DocIterator const & cur, int len) { + pos_type end_pos = cur.pos() + len; + for (pos_type pos = cur.pos(); pos != end_pos; ++pos) + if (isLowerCase(cur.paragraph().getChar(pos))) + return false; + return true; +} + + +/** Check if first letter is upper case and second one is lower case */ +static bool firstUppercase(DocIterator const & cur) { + char_type ch1, ch2; + if (cur.pos() >= cur.lastpos() - 1) { + LYXERR(Debug::FIND, "No upper-case at cur: " << cur); + return false; + } + ch1 = cur.paragraph().getChar(cur.pos()); + ch2 = cur.paragraph().getChar(cur.pos()+1); + bool result = isUpperCase(ch1) && isLowerCase(ch2); + LYXERR(Debug::FIND, "firstUppercase(): " + << "ch1=" << ch1 << "(" << char(ch1) << "), ch2=" + << ch2 << "(" << char(ch2) << ")" + << ", result=" << result << ", cur=" << cur); + return result; +} + + +/** Make first letter of supplied buffer upper-case, and the rest lower-case. + ** + ** \fixme What to do with possible further paragraphs in replace buffer ? + **/ +static void changeFirstCase(Buffer & buffer, TextCase first_case, TextCase others_case) { + ParagraphList::iterator pit = buffer.paragraphs().begin(); + pos_type right = pos_type(1); + pit->changeCase(buffer.params(), pos_type(0), right, first_case); + right = pit->size() + 1; + pit->changeCase(buffer.params(), right, right, others_case); +} + + +/// Perform a FindAdv operation. +bool findAdv(BufferView * bv, FindAndReplaceOptions const & opt) +{ + DocIterator cur = bv->cursor(); + int match_len = 0; + + if (opt.search.empty()) { + bv->message(_("Search text is empty!")); + return false; + } +// if (! bv->buffer()) { +// bv->message(_("No open document !")); +// return false; +// } + + MatchStringAdv matchAdv(bv->buffer(), opt); + try { + if (opt.forward) + match_len = findForwardAdv(cur, matchAdv); + else + match_len = findBackwardsAdv(cur, matchAdv); + } catch (...) { + // This may only be raised by boost::regex() + bv->message(_("Invalid regular expression!")); + return false; + } + + if (match_len == 0) { + bv->message(_("Match not found!")); + return false; + } + + LYXERR(Debug::FIND, "Putting selection at buf=" << matchAdv.p_buf + << "cur=" << cur << " with len: " << match_len); + + theApp()->currentWindow()->selectDocumentBuffer(matchAdv.p_buf); + bv = theApp()->currentWindow()->documentBufferView(); + + bv->putSelectionAt(cur, match_len, ! opt.forward); + if (opt.replace == docstring(from_utf8(LYX_FR_NULL_STRING))) { + bv->message(_("Match found !")); + } else { + string lyx = to_utf8(opt.replace); + // FIXME: Seems so stupid to me to rebuild a buffer here, + // when we already have one (replace_work_area_.buffer()) + Buffer repl_buffer("", false); + repl_buffer.setUnnamed(true); + if (repl_buffer.readString(lyx)) { + if (opt.keep_case && match_len >= 2) { + if (cur.inTexted()) { + if (firstUppercase(cur)) + changeFirstCase(repl_buffer, text_uppercase, text_lowercase); + else if (allNonLowercase(cur, match_len)) + changeFirstCase(repl_buffer, text_uppercase, text_uppercase); + } + } + cap::cutSelection(bv->cursor(), false, false); + if (! cur.inMathed()) { + LYXERR(Debug::FIND, "Replacing by pasteParagraphList()ing repl_buffer"); + cap::pasteParagraphList(bv->cursor(), repl_buffer.paragraphs(), + repl_buffer.params().documentClassPtr(), + bv->buffer().errorList("Paste")); + } else { + odocstringstream ods; + OutputParams runparams(&repl_buffer.params().encoding()); + runparams.nice = false; + runparams.flavor = OutputParams::LATEX; + runparams.linelen = 8000; //lyxrc.plaintext_linelen; + runparams.dryrun = true; + TexRow texrow; + TeXOnePar(repl_buffer, repl_buffer.text(), + repl_buffer.paragraphs().begin(), ods, texrow, runparams); + //repl_buffer.getSourceCode(ods, 0, repl_buffer.paragraphs().size(), false); + docstring repl_latex = ods.str(); + LYXERR(Debug::FIND, "Latexified replace_buffer: '" << repl_latex << "'"); + string s; + regex_replace(to_utf8(repl_latex), s, "\\$(.*)\\$", "$1"); + regex_replace(s, s, "\\\\\\[(.*)\\\\\\]", "$1"); + repl_latex = from_utf8(s); + LYXERR(Debug::FIND, "Replacing by niceInsert()ing latex: '" << repl_latex << "'"); + bv->cursor().niceInsert(repl_latex); + } + bv->putSelectionAt(cur, repl_buffer.paragraphs().begin()->size(), ! opt.forward); + bv->message(_("Match found and replaced !")); + } else + LASSERT(false, /**/); + // dispatch(FuncRequest(LFUN_SELF_INSERT, opt.replace)); + } + + return true; +} + + +void findAdv(BufferView * bv, FuncRequest const & ev) +{ + if (!bv || ev.action != LFUN_WORD_FINDADV) + return; + + FindAndReplaceOptions opt; + istringstream iss(to_utf8(ev.argument())); + iss >> opt; + findAdv(bv, opt); +} + + +ostringstream & operator<<(ostringstream & os, lyx::FindAndReplaceOptions const & opt) +{ + os << to_utf8(opt.search) << "\nEOSS\n" + << opt.casesensitive << ' ' + << opt.matchword << ' ' + << opt.forward << ' ' + << opt.expandmacros << ' ' + << opt.ignoreformat << ' ' + << opt.regexp << ' ' + << to_utf8(opt.replace) << "\nEOSS\n" + << opt.keep_case << ' ' + << int(opt.scope); + + LYXERR(Debug::FIND, "built: " << os.str()); + + return os; +} + +istringstream & operator>>(istringstream & is, lyx::FindAndReplaceOptions & opt) +{ + LYXERR(Debug::FIND, "parsing"); + string s; + string line; + getline(is, line); + while (line != "EOSS") { + if (! s.empty()) + s = s + "\n"; + s = s + line; + if (is.eof()) // Tolerate malformed request + break; + getline(is, line); + } + LYXERR(Debug::FIND, "searching for: '" << s << "'"); + opt.search = from_utf8(s); + is >> opt.casesensitive >> opt.matchword >> opt.forward >> opt.expandmacros >> opt.ignoreformat >> opt.regexp; + is.get(); // Waste space before replace string + s = ""; + getline(is, line); + while (line != "EOSS") { + if (! s.empty()) + s = s + "\n"; + s = s + line; + if (is.eof()) // Tolerate malformed request + break; + getline(is, line); + } + is >> opt.keep_case; + int i; + is >> i; + opt.scope = FindAndReplaceOptions::SearchScope(i); + LYXERR(Debug::FIND, "parsed: " << opt.casesensitive << ' ' << opt.matchword << ' ' << opt.forward << ' ' + << opt.expandmacros << ' ' << opt.ignoreformat << ' ' << opt.regexp << ' ' << opt.keep_case); + LYXERR(Debug::FIND, "replacing with: '" << s << "'"); + opt.replace = from_utf8(s); + return is; +} + } // lyx namespace