From ab3ef175cdd180ef66a4f4f11fe6257764005e33 Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Fri, 11 Jan 2013 15:54:28 -0800 Subject: [PATCH] Rewrite SearchReplaceEngine Decouple it from the subtitles grid and do everything through the project context instead. Actually obey the "In Selection" option for things other than Replace All. Cut down on the amount of logic duplicated between Replace All and normal find/replace. Start searching from the current cursor position, rather than the last found position. --- aegisub/src/search_replace_engine.cpp | 347 ++++++++++++-------------- aegisub/src/search_replace_engine.h | 10 +- 2 files changed, 168 insertions(+), 189 deletions(-) diff --git a/aegisub/src/search_replace_engine.cpp b/aegisub/src/search_replace_engine.cpp index db917ed42..274f9d971 100644 --- a/aegisub/src/search_replace_engine.cpp +++ b/aegisub/src/search_replace_engine.cpp @@ -1,29 +1,16 @@ -// Copyright (c) 2005, Rodrigo Braz Monteiro -// All rights reserved. +// Copyright (c) 2013, Thomas Goyne // -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. // -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// * Neither the name of the Aegisub Group nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // Aegisub Project http://www.aegisub.org/ @@ -42,18 +29,18 @@ #include #include -SearchReplaceEngine::SearchReplaceEngine(agi::Context *c) -: context(c) -, cur_line(0) -, pos(0) -, match_len(0) -, replace_len(0) -, last_was_find(true) -, initialized(false) -{ -} +struct MatchState { + wxRegEx *re; + size_t start, end; -static boost::flyweight *get_text(AssDialogue *cur, SearchReplaceSettings::Field field) { + MatchState() : re(nullptr), start(0), end(-1) { } + MatchState(size_t s, size_t e, wxRegEx *re = nullptr) : re(re), start(s), end(e) { } + operator bool() { return end != -1; } +}; + +namespace { +template +auto get_dialogue_field(AssDialogue *cur, SearchReplaceSettings::Field field) -> decltype(&cur->Text) { switch (field) { case SearchReplaceSettings::Field::TEXT: return &cur->Text; case SearchReplaceSettings::Field::STYLE: return &cur->Style; @@ -63,111 +50,146 @@ static boost::flyweight *get_text(AssDialogue *cur, SearchReplaceSetti throw agi::InternalError("Bad field for search", 0); } +std::function get_matcher(SearchReplaceSettings::Field field, wxString look_for, bool use_regex, bool match_case, wxRegEx *regex) { + if (use_regex) { + int flags = wxRE_ADVANCED; + if (!match_case) + flags |= wxRE_ICASE; + + regex->Compile(look_for, flags); + if (!regex->IsValid()) + return [](const AssDialogue*, size_t) { return MatchState(); }; + + return [=](const AssDialogue *diag, size_t start) { + auto const& str = *get_dialogue_field(diag, field); + if (!regex->Matches(str.get().substr(start))) + return MatchState(); + + size_t match_start, match_len; + regex->GetMatch(&match_start, &match_len, 0); + return MatchState(match_start + start, match_start + match_len + start, regex); + }; + } + + if (!match_case) + look_for.MakeLower(); + + return [=](const AssDialogue *diag, size_t start) { + auto str = get_dialogue_field(diag, field)->get().substr(start); + if (!match_case) + str.MakeLower(); + + size_t pos = str.find(look_for); + if (pos == wxString::npos) + return MatchState(); + + return MatchState(pos + start, pos + look_for.size() + start); + }; +} + +template +Iterator circular_next(Iterator it, Container& c) { + ++it; + if (it == c.end()) + it = c.begin(); + return it; +} + +} + +SearchReplaceEngine::SearchReplaceEngine(agi::Context *c) +: context(c) +, initialized(false) +{ +} + +void SearchReplaceEngine::Replace(AssDialogue *diag, MatchState &ms) { + auto diag_field = get_dialogue_field(diag, settings.field); + auto text = diag_field->get(); + + wxString replacement = settings.replace_with; + if (ms.re) { + wxString to_replace = text.substr(ms.start, ms.end - ms.start); + ms.re->ReplaceFirst(&to_replace, settings.replace_with); + replacement = to_replace; + } + + *diag_field = text.substr(0, ms.start) + replacement + text.substr(ms.end); + ms.end = ms.start + replacement.size(); +} + bool SearchReplaceEngine::FindReplace(bool replace) { if (!initialized) return false; - wxArrayInt sels = context->subsGrid->GetSelection(); - int firstLine = sels.empty() ? 0 : sels.front(); + wxRegEx r; + auto matches = get_matcher(settings.field, settings.find, settings.use_regex, settings.match_case, &r); - // if selection has changed reset values - if (firstLine != cur_line) { - cur_line = firstLine; - last_was_find = true; - pos = 0; - match_len = 0; - replace_len = 0; - } + AssDialogue *line = context->selectionController->GetActiveLine(); + auto it = context->ass->Line.iterator_to(*line); + size_t pos = 0; - // Setup - int start = cur_line; - int nrows = context->subsGrid->GetRows(); - bool found = false; - int regFlags = wxRE_ADVANCED; - if (!settings.match_case) { - if (settings.use_regex) - regFlags |= wxRE_ICASE; - else - settings.find.MakeLower(); - } - wxRegEx regex; - if (settings.use_regex) { - regex.Compile(settings.find, regFlags); + MatchState replace_ms; + if (replace) { + if (settings.field == SearchReplaceSettings::Field::TEXT) + pos = context->textSelectionController->GetSelectionStart(); - if (!regex.IsValid()) { - last_was_find = !replace; - return true; - } - } + if ((replace_ms = matches(line, pos))) { + size_t end = -1; + if (settings.field == SearchReplaceSettings::Field::TEXT) + end = context->textSelectionController->GetSelectionEnd(); - // Search for it - boost::flyweight *Text = nullptr; - while (!found) { - Text = get_text(context->subsGrid->GetDialogue(cur_line), settings.field); - size_t tempPos; - if (replace && last_was_find) - tempPos = pos; - else - tempPos = pos + replace_len; - - if (settings.use_regex) { - if (regex.Matches(Text->get().substr(tempPos))) { - size_t match_start; - regex.GetMatch(&match_start, &match_len, 0); - pos = match_start + tempPos; - found = true; - } - } - else { - wxString src = Text->get().substr(tempPos); - if (!settings.match_case) src.MakeLower(); - size_t textPos = src.find(settings.find); - if (textPos != src.npos) { - pos = tempPos+textPos; - found = true; - match_len = settings.find.size(); - } - } - - // Didn't find, go to next line - if (!found) { - cur_line = (cur_line + 1) % nrows; - pos = 0; - match_len = 0; - replace_len = 0; - if (cur_line == start) break; - } - } - - if (found) { - if (!replace) - replace_len = match_len; - else { - if (settings.use_regex) { - wxString toReplace = Text->get().substr(pos,match_len); - regex.ReplaceFirst(&toReplace,settings.replace_with); - *Text = Text->get().Left(pos) + toReplace + Text->get().substr(pos+match_len); - replace_len = toReplace.size(); + if (end != -1 || (pos == replace_ms.start && end == replace_ms.end)) { + Replace(line, replace_ms); + pos = replace_ms.end; + context->ass->Commit(_("replace"), AssFile::COMMIT_DIAG_TEXT); } else { - *Text = Text->get().Left(pos) + settings.replace_with + Text->get().substr(pos+match_len); - replace_len = settings.replace_with.size(); + // The current line matches, but it wasn't already selected, + // so the match hasn't been "found" and displayed to the user + // yet, so do that rather than replacing + context->textSelectionController->SetSelection(replace_ms.start, replace_ms.end); + return true; + } + } + } + // Search from the end of the selection to avoid endless matching the same thing + else if (settings.field == SearchReplaceSettings::Field::TEXT) + pos = context->textSelectionController->GetSelectionEnd(); + // For non-text fields we just look for matching lines rather than each + // match within the line, so move to the next line + else if (settings.field != SearchReplaceSettings::Field::TEXT) + it = circular_next(it, context->ass->Line); + + auto const& sel = context->selectionController->GetSelectedSet(); + bool selection_only = settings.limit_to == SearchReplaceSettings::Limit::SELECTED; + + do { + AssDialogue *diag = dynamic_cast(&*it); + if (!diag) continue; + if (selection_only && !sel.count(diag)) continue; + + if (MatchState ms = matches(diag, pos)) { + if (selection_only) + // We're cycling through the selection, so don't muck with it + context->selectionController->SetActiveLine(diag); + else { + SubtitleSelection new_sel; + new_sel.insert(diag); + context->selectionController->SetSelectionAndActive(new_sel, diag); } - context->ass->Commit(_("replace"), AssFile::COMMIT_DIAG_TEXT); - } + if (settings.field == SearchReplaceSettings::Field::TEXT) + context->textSelectionController->SetSelection(ms.start, ms.end); - context->subsGrid->SelectRow(cur_line,false); - context->subsGrid->MakeCellVisible(cur_line,0); - if (settings.field == SearchReplaceSettings::Field::TEXT) { - context->selectionController->SetActiveLine(context->subsGrid->GetDialogue(cur_line)); - context->textSelectionController->SetSelection(pos, pos + replace_len); + return true; } - // hAx to prevent double match on style/actor - else - replace_len = 99999; - } - last_was_find = !replace; + } while (pos = 0, &*(it = circular_next(it, context->ass->Line)) != line); + + // Replaced something and didn't find another match, so select the newly + // inserted text + if (replace_ms && settings.field == SearchReplaceSettings::Field::TEXT) + context->textSelectionController->SetSelection(replace_ms.start, replace_ms.end); return true; } @@ -178,63 +200,33 @@ bool SearchReplaceEngine::ReplaceAll() { size_t count = 0; - int regFlags = wxRE_ADVANCED; - if (!settings.match_case) - regFlags |= wxRE_ICASE; - wxRegEx reg; - if (settings.use_regex) - reg.Compile(settings.find, regFlags); + wxRegEx r; + auto matches = get_matcher(settings.field, settings.find, settings.use_regex, settings.match_case, &r); SubtitleSelection const& sel = context->selectionController->GetSelectedSet(); - bool hasSelection = !sel.empty(); - bool inSel = settings.limit_to == SearchReplaceSettings::Limit::SELECTED; + bool selection_only = settings.limit_to == SearchReplaceSettings::Limit::SELECTED; for (auto diag : context->ass->Line | agi::of_type()) { - if (inSel && hasSelection && !sel.count(diag)) - continue; - - boost::flyweight *Text = get_text(diag, settings.field); + if (selection_only && !sel.count(diag)) continue; if (settings.use_regex) { - if (reg.Matches(*Text)) { - size_t start, len; - reg.GetMatch(&start, &len); - + if (MatchState ms = matches(diag, 0)) { + auto diag_field = get_dialogue_field(diag, settings.field); + auto text = diag_field->get(); // A zero length match (such as '$') will always be replaced // maxMatches times, which is almost certainly not what the user // wanted, so limit it to one replacement in that situation - wxString repl(*Text); - count += reg.Replace(&repl, settings.replace_with, len > 0 ? 1000 : 1); - *Text = repl; + count += ms.re->Replace(&text, settings.replace_with, ms.start == ms.end); + *diag_field = text; } + continue; } - else { - if (!settings.match_case) { - bool replaced = false; - wxString Left, Right = *Text; - size_t pos = 0; - Left.reserve(Right.size()); - while (pos + settings.find.size() <= Right.size()) { - if (Right.substr(pos, settings.find.size()).CmpNoCase(settings.find) == 0) { - Left.Append(Right.Left(pos)).Append(settings.replace_with); - Right = Right.substr(pos + settings.find.size()); - ++count; - replaced = true; - pos = 0; - } - else { - pos++; - } - } - if (replaced) { - *Text = Left + Right; - } - } - else if(Text->get().Contains(settings.find)) { - wxString repl(*Text); - count += repl.Replace(settings.find, settings.replace_with); - *Text = repl; - } + + size_t pos = 0; + while (MatchState ms = matches(diag, pos)) { + ++count; + Replace(diag, ms); + pos = ms.end; } } @@ -245,20 +237,11 @@ bool SearchReplaceEngine::ReplaceAll() { else { wxMessageBox(_("No matches found.")); } - last_was_find = false; return true; } void SearchReplaceEngine::Configure(SearchReplaceSettings const& new_settings) { - wxArrayInt sels = context->subsGrid->GetSelection(); - cur_line = 0; - if (sels.size() > 0) cur_line = sels[0]; - - last_was_find = true; - pos = 0; - match_len = 0; - replace_len = 0; - settings = new_settings; + initialized = true; } diff --git a/aegisub/src/search_replace_engine.h b/aegisub/src/search_replace_engine.h index 5a9361bd7..af023f842 100644 --- a/aegisub/src/search_replace_engine.h +++ b/aegisub/src/search_replace_engine.h @@ -17,6 +17,8 @@ #include namespace agi { struct Context; } +class AssDialogue; +struct MatchState; struct SearchReplaceSettings { enum class Field { @@ -43,17 +45,11 @@ struct SearchReplaceSettings { class SearchReplaceEngine { agi::Context *context; - - int cur_line; - size_t pos; - size_t match_len; - size_t replace_len; - bool last_was_find; bool initialized; - SearchReplaceSettings settings; bool FindReplace(bool replace); + void Replace(AssDialogue *line, MatchState &ms); public: bool FindNext() { return FindReplace(false); }