diff --git a/aegisub/src/dialog_search_replace.cpp b/aegisub/src/dialog_search_replace.cpp index 3ae685bda..4e6e5cf4c 100644 --- a/aegisub/src/dialog_search_replace.cpp +++ b/aegisub/src/dialog_search_replace.cpp @@ -57,6 +57,7 @@ DialogSearchReplace::DialogSearchReplace(agi::Context* c, bool replace) settings->match_case = OPT_GET("Tool/Search Replace/Match Case")->GetBool(); settings->use_regex = OPT_GET("Tool/Search Replace/RegExp")->GetBool(); settings->ignore_comments = OPT_GET("Tool/Search Replace/Skip Comments")->GetBool(); + settings->skip_tags = OPT_GET("Tool/Search Replace/Skip Tags")->GetBool(); auto find_sizer = new wxFlexGridSizer(2, 2, 5, 15); find_edit = new wxComboBox(this, -1, "", wxDefaultPosition, wxSize(300, -1), recent_find, wxCB_DROPDOWN, wxGenericValidator(&settings->find)); @@ -72,7 +73,8 @@ DialogSearchReplace::DialogSearchReplace(agi::Context* c, bool replace) auto options_sizer = new wxBoxSizer(wxVERTICAL); options_sizer->Add(new wxCheckBox(this, -1, _("&Match case"), wxDefaultPosition, wxDefaultSize, 0, wxGenericValidator(&settings->match_case)), wxSizerFlags().Border(wxBOTTOM)); options_sizer->Add(new wxCheckBox(this, -1, _("&Use regular expressions"), wxDefaultPosition, wxDefaultSize, 0, wxGenericValidator(&settings->use_regex)), wxSizerFlags().Border(wxBOTTOM)); - options_sizer->Add(new wxCheckBox(this, -1, _("&Skip Comments"), wxDefaultPosition, wxDefaultSize, 0, wxGenericValidator(&settings->ignore_comments))); + options_sizer->Add(new wxCheckBox(this, -1, _("&Skip Comments"), wxDefaultPosition, wxDefaultSize, 0, wxGenericValidator(&settings->ignore_comments)), wxSizerFlags().Border(wxBOTTOM)); + options_sizer->Add(new wxCheckBox(this, -1, _("S&kip Override Tags"), wxDefaultPosition, wxDefaultSize, 0, wxGenericValidator(&settings->skip_tags))); auto left_sizer = new wxBoxSizer(wxVERTICAL); left_sizer->Add(find_sizer, wxSizerFlags().DoubleBorder(wxBOTTOM)); @@ -134,6 +136,7 @@ void DialogSearchReplace::FindReplace(bool (SearchReplaceEngine::*func)()) { OPT_SET("Tool/Search Replace/Match Case")->SetBool(settings->match_case); OPT_SET("Tool/Search Replace/RegExp")->SetBool(settings->use_regex); OPT_SET("Tool/Search Replace/Skip Comments")->SetBool(settings->ignore_comments); + OPT_SET("Tool/Search Replace/Skip Tags")->SetBool(settings->skip_tags); OPT_SET("Tool/Search Replace/Field")->SetInt(static_cast(settings->field)); OPT_SET("Tool/Search Replace/Affect")->SetInt(static_cast(settings->limit_to)); diff --git a/aegisub/src/libresrc/default_config.json b/aegisub/src/libresrc/default_config.json index edd950792..b87945eec 100644 --- a/aegisub/src/libresrc/default_config.json +++ b/aegisub/src/libresrc/default_config.json @@ -463,7 +463,8 @@ "Field" : 0, "Match Case" : false, "RegExp" : false, - "Skip Comments" : false + "Skip Comments" : false, + "Skip Tags" : false }, "Select Lines" : { "Action" : 0, diff --git a/aegisub/src/libresrc/osx/default_config.json b/aegisub/src/libresrc/osx/default_config.json index f9bb55843..793d61162 100644 --- a/aegisub/src/libresrc/osx/default_config.json +++ b/aegisub/src/libresrc/osx/default_config.json @@ -463,7 +463,8 @@ "Field" : 0, "Match Case" : false, "RegExp" : false, - "Skip Comments" : false + "Skip Comments" : false, + "Skip Tags" : false }, "Select Lines" : { "Action" : 0, diff --git a/aegisub/src/search_replace_engine.cpp b/aegisub/src/search_replace_engine.cpp index 3ec243688..369f4c49c 100644 --- a/aegisub/src/search_replace_engine.cpp +++ b/aegisub/src/search_replace_engine.cpp @@ -29,13 +29,15 @@ #include #include +static const size_t bad_pos = -1; + struct MatchState { wxRegEx *re; size_t start, end; 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; } + MatchState(size_t s, size_t e, wxRegEx *re) : re(re), start(s), end(e) { } + operator bool() { return end != bad_pos; } }; namespace { @@ -50,32 +52,123 @@ auto get_dialogue_field(AssDialogue *cur, SearchReplaceSettings::Field field) -> 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) { +typedef std::function matcher; + +struct noop_accessor { + SearchReplaceSettings::Field field; + size_t start; + + wxString get(const AssDialogue *d, size_t s) { + start = s; + return get_dialogue_field(d, field)->get().substr(s); + } + + MatchState make_match_state(size_t s, size_t e, wxRegEx *r = nullptr) { + return MatchState(s + start, e + start, r); + } +}; + +struct skip_tags_accessor { + SearchReplaceSettings::Field field; + std::vector> blocks; + size_t start; + + void parse_str(wxString const& str) { + blocks.clear(); + + size_t ovr_start = bad_pos; + size_t i = 0; + for (auto const& c : str) { + if (c == '{' && ovr_start == bad_pos) + ovr_start = i; + else if (c == '}' && ovr_start != bad_pos) { + blocks.emplace_back(ovr_start, i); + ovr_start = bad_pos; + } + ++i; + } + } + + wxString get(const AssDialogue *d, size_t s) { + auto const& str = get_dialogue_field(d, field)->get(); + parse_str(str); + + wxString out; + + size_t last = s; + for (auto const& block : blocks) { + if (block.second < s) continue; + if (block.first > last) + out.append(str.begin() + last, str.begin() + block.first); + last = block.second + 1; + } + + if (last < str.size()) + out.append(str.begin() + last, str.end()); + + start = s; + return out; + } + + MatchState make_match_state(size_t s, size_t e, wxRegEx *r = nullptr) { + s += start; + e += start; + + // Shift the start and end of the match to be relative to the unstripped + // match + for (auto const& block : blocks) { + // Any blocks before start are irrelevant as they're included in `start` + if (block.second < s) continue; + // Skip over blocks at the very beginning of the match + // < should only happen if the cursor was within an override block + // when the user started a search + if (block.first <= s) { + size_t len = block.second - std::max(block.first, s) + 1; + s += len; + e += len; + continue; + } + + assert(block.first > s); + // Blocks after the match are irrelevant + if (block.first >= e) break; + + // Extend the match to include blocks within the match + // Note that blocks cannot be partially within the match + e += block.second - block.first + 1; + } + return MatchState(s, e, r); + } +}; + +template +matcher get_matcher(SearchReplaceSettings const& settings, wxRegEx *regex, Accessor a) { + if (settings.use_regex) { int flags = wxRE_ADVANCED; - if (!match_case) + if (!settings.match_case) flags |= wxRE_ICASE; - regex->Compile(look_for, flags); + regex->Compile(settings.find, 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 [=](const AssDialogue *diag, size_t start) mutable -> MatchState { + if (!regex->Matches(a.get(diag, 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); + return a.make_match_state(match_start, match_start + match_len, regex); }; } - if (!match_case) + bool match_case = settings.match_case; + wxString look_for = settings.find; + if (!settings.match_case) look_for.MakeLower(); - return [=](const AssDialogue *diag, size_t start) { - auto str = get_dialogue_field(diag, field)->get().substr(start); + return [=](const AssDialogue *diag, size_t start) mutable -> MatchState { + auto str = a.get(diag, start); if (!match_case) str.MakeLower(); @@ -83,10 +176,20 @@ std::function get_matcher(SearchReplace if (pos == wxString::npos) return MatchState(); - return MatchState(pos + start, pos + look_for.size() + start); + return a.make_match_state(pos, pos + look_for.size()); }; } +matcher get_matcher(SearchReplaceSettings const& settings, wxRegEx *regex) { + if (!settings.skip_tags) { + noop_accessor a = { settings.field }; + return get_matcher(settings, regex, a); + } + + skip_tags_accessor a = { settings.field }; + return get_matcher(settings, regex, a); +} + template Iterator circular_next(Iterator it, Container& c) { ++it; @@ -123,7 +226,7 @@ bool SearchReplaceEngine::FindReplace(bool replace) { return false; wxRegEx r; - auto matches = get_matcher(settings.field, settings.find, settings.use_regex, settings.match_case, &r); + auto matches = get_matcher(settings, &r); AssDialogue *line = context->selectionController->GetActiveLine(); auto it = context->ass->Line.iterator_to(*line); @@ -135,11 +238,11 @@ bool SearchReplaceEngine::FindReplace(bool replace) { pos = context->textSelectionController->GetSelectionStart(); if ((replace_ms = matches(line, pos))) { - size_t end = -1; + size_t end = bad_pos; if (settings.field == SearchReplaceSettings::Field::TEXT) end = context->textSelectionController->GetSelectionEnd(); - if (end != -1 || (pos == replace_ms.start && end == replace_ms.end)) { + if (end == bad_pos || (pos == replace_ms.start && end == replace_ms.end)) { Replace(line, replace_ms); pos = replace_ms.end; context->ass->Commit(_("replace"), AssFile::COMMIT_DIAG_TEXT); @@ -202,7 +305,7 @@ bool SearchReplaceEngine::ReplaceAll() { size_t count = 0; wxRegEx r; - auto matches = get_matcher(settings.field, settings.find, settings.use_regex, settings.match_case, &r); + auto matches = get_matcher(settings, &r); SubtitleSelection const& sel = context->selectionController->GetSelectedSet(); bool selection_only = settings.limit_to == SearchReplaceSettings::Limit::SELECTED; diff --git a/aegisub/src/search_replace_engine.h b/aegisub/src/search_replace_engine.h index aea271ff7..11e7fe76c 100644 --- a/aegisub/src/search_replace_engine.h +++ b/aegisub/src/search_replace_engine.h @@ -42,6 +42,7 @@ struct SearchReplaceSettings { bool match_case; bool use_regex; bool ignore_comments; + bool skip_tags; }; class SearchReplaceEngine {