Skip to content

Fix Replace & find next silently skipping current match#11399

Merged
niksedk merged 2 commits into
mainfrom
fix-replace-find-next-11388
Jun 4, 2026
Merged

Fix Replace & find next silently skipping current match#11399
niksedk merged 2 commits into
mainfrom
fix-replace-find-next-11388

Conversation

@niksedk
Copy link
Copy Markdown
Member

@niksedk niksedk commented Jun 4, 2026

Summary

Fixes #11388. In the Edit → Replace dialog, the Replace button (which acts as "Replace & find next") was driven by EditTextBox.SelectedText and EditTextBox.SelectionStart. When that UI state didn't perfectly match the last find result — focus changes, line-ending normalization on a multi-line subtitle, an in-flight TextBox text update — the guard selectedText == savedCurrentTextFound failed, the replacement block was skipped entirely, and FindNext still ran and advanced past the unmodified match. The user's reproducer (regex ([?!\.,])$1 against coming ! Look!) hits exactly this path.

What changed

HandleReplaceResult now drives the replace path from the FindService's saved match — CurrentLineNumber / CurrentTextIndex / CurrentTextFound, captured before Initialize wipes them. The replacement is applied to the underlying subtitle text (Subtitles[line].Text), and FindNext resumes from the position immediately after the replacement.

Both modes also defensively verify the saved match still describes what's actually in the line, so an in-flight edit to the subtitle can't be corrupted:

  • Regex: re-runs the pattern at savedFoundIndex and bails unless match.Success && match.Index == savedFoundIndex. match.Result(replacement) properly expands $1/$2/etc.
  • Non-regex: compares line.Substring(savedFoundIndex, savedFoundText.Length) to the saved match under the configured case-sensitivity mode and bails on mismatch.

Side benefit: the non-regex path previously did EditTextBox.SelectedText = ReplaceText, which when the selection had collapsed just inserted at the caret instead of replacing. That class of bug goes away too.

Test plan

  • Reproduce the original issue: paste the user's two-line subtitle, set regex ([?!\\.,])\$1, ensure case-insensitive regex mode. Click Find next, then Replace. Confirm the space is removed and the cursor advances.
  • Click Replace repeatedly across multiple matches in different subtitles. Each press should replace the current match and jump to the next.
  • Non-regex case-insensitive: Find a word, click Replace, confirm replacement.
  • Non-regex case-sensitive: same.
  • Replace All still works.
  • Find next (without Replace) still works.
  • Edit the subtitle text manually between Find and Replace — the replace should bail rather than corrupt the edited line.
  • Build clean; libse tests pass (367/367); UI tests pass (306/306).

🤖 Generated with Claude Code

The Replace button on the Edit > Replace dialog (which acts as
"Replace and find next") read EditTextBox.SelectedText /
EditTextBox.SelectionStart to figure out what to replace and where.
Two failure modes followed:

1. If the EditTextBox selection didn't exactly equal the last-found
   text (focus changes, async TextBox updates, normalized line endings
   shifting indices on multi-line subtitles, etc.), the
   `selectedText == savedCurrentTextFound` guard failed, the entire
   replacement block was skipped, and FindNext still ran — advancing
   the cursor to the next match without replacing.
2. The non-regex path wrote `EditTextBox.SelectedText = ReplaceText`,
   which when the selection had collapsed just inserted at the caret
   instead of replacing.

Drive the replace path from the FindService's saved match (line
index, text index, found text) instead. These are captured before
Initialize wipes them and are the authoritative description of the
match the user just navigated to. The replacement is applied to the
underlying subtitle text (`Subtitles[line].Text`), and `FindNext`
resumes from the position immediately after the replacement.

The new code also guards both modes:
- For regex it re-runs the match at the saved index and bails if it
  doesn't start exactly there (so an in-flight edit can't be corrupted).
- For non-regex it verifies the slice at the saved index still equals
  the matched text under the configured case mode before mutating.

Fixes #11388

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes issue #11388 where Replace & find next could skip the current match (without replacing) due to relying on EditTextBox selection state instead of the last saved find result.

Changes:

  • Drives single-replace from _findService’s saved match (CurrentLineNumber / CurrentTextIndex / CurrentTextFound) captured before _findService.Initialize(...) resets state.
  • Applies the replacement directly to the underlying subtitle line (Subtitles[line].Text) and resumes FindNext from immediately after the replacement.
  • Adds defensive “match still valid” checks for non-regex (and attempts similar for regex) to avoid corrupting text if the subtitle was edited between Find and Replace.

Comment on lines +9796 to +9818
if (result.FindMode == FindMode.RegularExpression)
{
try
{
var fixedReplaceText = RegexUtils.FixNewLine(result.ReplaceText);
var regex = new Regex(result.SearchText);
var match = regex.Match(line, matchIndex);
if (!match.Success || match.Index != matchIndex)
{
newLine = line;
return null;
}

var replaced = match.Result(fixedReplaceText);
newLine = line.Substring(0, matchIndex) + replaced + line.Substring(matchIndex + match.Length);
return replaced.Length;
}
catch (ArgumentException)
{
newLine = line;
return null;
}
}
Addresses Copilot review feedback on #11399: the regex replace path
re-ran the pattern at the saved match index and only checked that the
new match started at that exact offset. Two gaps:

1. If the subtitle was edited between Find and Replace, a different
   substring of the same length could match at the same offset and be
   silently replaced.
2. The FindService normalizes CRLF -> LF before regex matching and
   maps the index back; the re-match here runs against the
   un-normalized line, so for line-ending-sensitive patterns the
   indices can drift even when the same logical match still exists.

Compare match.Value (ordinal) to the saved found text and bail on
mismatch. The non-regex path already had an equivalent check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@niksedk niksedk merged commit 89a2b3d into main Jun 4, 2026
1 of 3 checks passed
@niksedk niksedk deleted the fix-replace-find-next-11388 branch June 4, 2026 19:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[SE5 RC2] "Replace & find next" broken

2 participants