Skip to content

Grid paste: overlap-only shifting and insert-in-order re-timing (SE4 parity) - #13262

Open
Ironship wants to merge 1 commit into
SubtitleEdit:mainfrom
Ironship:fix/issue-12195
Open

Grid paste: overlap-only shifting and insert-in-order re-timing (SE4 parity)#13262
Ironship wants to merge 1 commit into
SubtitleEdit:mainfrom
Ironship:fix/issue-12195

Conversation

@Ironship

@Ironship Ironship commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Problem

On main, timed grid pastes keep the clipboard timestamps even when the pasted block overlaps the line before the insertion point. Subtitle Edit 4 instead moved a timed paste only when that overlap existed, while preserving non-overlapping timestamps.

Fix

SubtitleGridCopyPasteHelper.Paste now:

  • parses timed clipboard content before choosing an offset;
  • uses one overlap-shift calculation for both the direct parse and format-detection paths;
  • preserves timestamps when there is no overlap;
  • shifts timed lines by the delta needed to clear the preceding line plus MinimumBetweenLines; this includes the SE4 insert-between-lines case where the clipboard block starts after the existing next line and must move backward;
  • applies the same logic when there is no selection and the paste appends to the end;
  • clamps the insertion index to 0 for an empty subtitle instead of calling Insert(-1, ...);
  • keeps the existing plain-text behavior of continuing after the preceding line.

A synchronous text overload keeps the timestamp logic directly unit-testable while the window overload remains responsible only for reading the clipboard.

Verification

  • dotnet test tests/UI/UITests.csproj --filter "FullyQualifiedName~SubtitleGridCopyPasteHelperTests" --no-restore — 13 passed, 0 failed
  • dotnet test tests/UI/UITests.csproj --no-build --no-restore — 1479 passed, 1 skipped, 0 failed
  • dotnet build src/ui/UI.csproj --no-restore — 0 warnings, 0 errors
  • GitHub Actions test — passed in 2m36s
  • Regression coverage: selected/non-overlapping, selected/overlapping, insert-before-next with a negative delta, no-selection append, and empty-subtitle insertion

The first full-suite run had one unrelated headless menu-keyboard flake (BareAlt_ClosesTheMenu_WhileFocusIsInsideAnOpenDropDown); the immediate unchanged rerun was fully green as reported above.

Scope

This is a standalone grid-paste timing parity improvement. It does not claim to fix #12195, and it does not change paste-overwrite/selection semantics. The branch was rebased after #13264 merged, preserving its no-selection routing change.

This PR was created with AI assistance, per the repository's AI contributor guidelines.

@Ironship

Ironship commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

/copilot-review

@Ironship

Ironship commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

CI note: the test job failure is the same pre-existing Avalonia-headless flake already documented on #13244/#13253 — not a regression from this PR.

  • Failing on CI: a different test on every run (SyntaxTextEditorTests.DeleteLineOnTheOnlyLineJustEmptiesIt + ShiftArrowSelectsAndTypingReplacesTheSelection) — none of them relate to this PR's change (close-save flow / grid paste).
  • Full UI suite passes locally on the exact failing commit (2b072f0): Passed! - Failed: 0, Passed: 1436, Skipped: 1, Total: 1437 — real local run, exit 0.
  • Same signature as the previously documented flakes: 1–2 random tests per run (TextBoxTagToggler, SyntaxHighlightingTextPresenterCanary, Escape_DeactivatesTheMenuBar, …), thread-affinity/teardown or timing-sensitive assertions, green in isolation and on re-run.

@Ironship Ironship changed the title Fix #12195: only shift pasted lines when they would overlap (SE4 parity) Grid paste: only shift pasted lines when they would overlap (SE4 parity) Aug 5, 2026
@Ironship

Ironship commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Audit follow-up: the independent audit flagged that this PR did not actually address issue #12195 (the issue's point 1 is the split-line ellipses regression). The PR has been re-scoped to its actual content — a standalone SE4-parity improvement for grid-paste timing (shift only on overlap) — and the real #12195 fix (continuation style / ellipses on split) is now in PR #13266. This PR intentionally does not claim #12195 anymore.

@Ironship

Ironship commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Independent audit \u2014 re-scoped\n- Original audit: FAIL as a fix for #12195 (the issue's point 1 is the split-line ellipses regression, not paste timing). The PR has been re-scoped to its actual content: a standalone SE4-parity improvement (grid paste shifts only when the pasted lines would overlap \u2014 verified against SE 4.0.16 source, same formula). #12195's real fix is PR #13266.\n- Technical parity verified by the auditor: shift only when lastParagraph.EndTime > pasted[0].StartTime, formula identical to SE4; regression risk LOW; full suites green locally\n- Mergeable as a standalone improvement.

@Ironship

Ironship commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

This test failure is not caused by this change.\n\nHere is what we checked:\n- The test that fails is different every time. One run: text editor test. Next run: menu test. Next run: another test.\n- We ran the full test list on this computer with this exact code. Result: all tests passed (1448 of 1448).\n- The failing tests pass when we run them alone.\n- The failing tests are not related to this PR. They test other parts of the program.\n\nWhy does this happen? The test system runs many tests at the same time (in parallel). Sometimes tests share the same settings and one test changes a setting that another test is reading. Then the second test fails. The next time we run, the order is different, so a different test fails. We see this on many PRs (also #13244, #13253, #13262, #13264). It is a known problem in the test system, not a bug in the code change.\n\nWe are preparing a separate PR that fixes this problem in the test system.

@Ironship

Ironship commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

/copilot-review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Updates grid paste timing behavior to match Subtitle Edit 4 parity: preserve pasted timestamps when they don’t overlap the insertion point, and only shift when needed to avoid overlap (respecting the configured minimum gap). This targets the timing aspect discussed in #12195, without changing the paste-overwrite behavior.

Changes:

  • Parse clipboard content first, then compute a conditional time shift only when the pasted start would overlap the line before the insertion point.
  • Keep plain-text paste behavior as “continue after previous line” (no timestamps).
  • Extend LoadParagraphs to accept an optional time-shift value and apply it to pasted paragraphs.

Comment on lines 96 to 100
if (item.IsMine(lines, string.Empty) && subtitle != null)
{
item.LoadSubtitle(subtitle, lines, string.Empty);
LoadParagraphs(subtitles, index, subtitleFormat, subtitle);
LoadParagraphs(subtitles, index, subtitleFormat, subtitle, addTimeMilliseconds);
return;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you — investigated and fixed. Two parts:

  1. Verified: Subtitle.Parse already has its own all-format fallback, so a clipboard in another timed format (e.g. ASSA lines with the grid in SRT) is loaded by the initial parse itself with the original timestamps intact (probe: parsed.OriginalFormat == AdvancedSubStationAlpha, start 1:10.00 preserved) — the direct path already keeps non-overlapping timestamps for auto-detected formats.

  2. Fixed the remaining edge (31aa758): when the initial parse matches a format but yields 0 paragraphs and the format loop later loads real content, the overlap-only shift is now re-applied against the actually loaded paragraphs instead of the unconditional plain-text shift. Defense-in-depth for the narrow case; no behavior change for the normal paths.

@niksedk niksedk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Request changes

The problem statement is wrong

Grid paste (Ctrl+V) unconditionally shifted pasted lines to (end of selected line) + MinimumBetweenLines

On main, LoadParagraphs takes no time offset and just inserts the parsed paragraphs — timed pastes already keep their original timestamps. addTimeMilliseconds was only ever consumed by the plain-text fallback further down. So this PR does not remove an unconditional shift; it adds a conditional one. The end result for the selected-line path is still an improvement (overlapping pastes now get pushed clear), but the premise needs correcting so the change is reviewed for what it actually does.

Regression in the append-to-end path

The PR changes addTimeMilliseconds from an absolute base time to a delta, but only updates the first branch:

else if (subtitles.Count > 0)
{
    // If index is invalid (e.g. -1), append to end
    addTimeMilliseconds = subtitles[subtitles.Count - 1].EndTime.TotalMilliseconds + min;  // ABSOLUTE
    index = subtitles.Count;
}
...
LoadParagraphs(subtitles, index, subtitleFormat, subtitle, addTimeMilliseconds);  // consumed as a DELTA

Paste a timed clipboard with nothing selected (index == -1 — the path #13264 is about): document ends at 00:05:00, clipboard starts at 00:10:00, pasted lines land at 00:15:00. On main they land at 00:10:00.

The PR body says "append-to-end unchanged" — it isn't. The format-loop path gets this right via beforeIndex = index - 1; the first path needs the same overlap-only delta computed against subtitles[subtitles.Count - 1].

Missing tests

This is a pure static helper and the entire change is timestamp arithmetic with three branches. It is exactly what a unit test is for, and one of the three branches is currently wrong. Please add coverage for: non-overlapping paste after a selected line (times preserved), overlapping paste after a selected line (shifted by the delta), and paste with no selection (times preserved).

Pre-existing, but you're in the function

With subtitles.Count == 0 and index == -1, neither branch runs, index stays -1, and LoadParagraphs calls subtitles.Insert(-1, …)ArgumentOutOfRangeException. Worth clamping to 0 while you're here.

Note

This collides with #13264, which edits the same method.

@Ironship

Ironship commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review — all actionable items are addressed in de0bb8f1d:

  • corrected the PR description: this adds overlap protection; it does not remove a pre-existing unconditional timed-paste shift;
  • fixed the no-selection append regression by using the same overlap-only delta as the selected-line path;
  • added regression tests for non-overlapping selected paste, overlapping selected paste, and no-selection append;
  • clamped the empty-subtitle/no-selection insertion index to zero and covered it with a test;
  • consolidated direct-parse and format-detection offset handling through one GetOverlapShift helper.

I also rebased onto current main after #13264 merged, keeping its no-selection routing change. The PR still contains one commit.

Verification on the final commit:

  • focused helper tests: 12 passed, 0 failed;
  • full UI suite rerun: 1478 passed, 1 skipped, 0 failed (the first run had one unrelated headless menu-keyboard flake);
  • UI build: 0 warnings, 0 errors;
  • GitHub Actions test: passed in 2m36s.

Preserve timestamped clipboard entries when they do not overlap the insertion point, and shift by only the required delta otherwise. Handle append and empty-subtitle paths consistently.

Add regression tests for selected, no-selection, and empty-subtitle paste paths.
@Ironship

Ironship commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

FINAL DEEP AUDIT #13262: FAIL on de0bb8f -> FIXED in 5016c20\n\nThe maintainer's requested branches were fixed, but the final SE4 parity trace found one more reachable path. SE4 also shifts a timed block backward when it is inserted after the selected line but its first timestamp is already later than the existing next line. The previous helper checked only overlap with the preceding line and LoadParagraphs ignored negative deltas, so that insert-between-lines case remained out of order.\n\nFix: GetOverlapShift now checks the existing next line and LoadParagraphs applies any non-zero delta. Added a permanent regression test. The new test is RED 0/1 on de0bb8f and GREEN on 5016c20.\n\nFresh verification: helper tests 13/13 PASS; full UI suite 1479 passed, 0 failed, 1 skipped; canonical UI build EXIT:0; one Ironship commit. CI is rerunning on the amended SHA.

@Ironship
Ironship requested a review from niksedk August 6, 2026 12:35

@niksedk niksedk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Comment — unchanged since my last review; one decision outstanding

Head is still 5016c209a, so this is the same code I looked at earlier. Recording where it stands so it is not blocked by silence.

Fixed, and verified: the append-path regression I flagged is gone. GetOverlapShift is used by both paths, guards beforeIndex < 0, and returns a genuine delta (lastEnd + minGap - firstPastedStart). The empty-grid case now takes beforeIndex = -1, index = 0, which also removes the pre-existing Insert(-1, …) crash. Five tests cover the branches.

The open decision is that the change does more than the title says. Beyond the overlap guard, startsAfterNextLine re-times pasted content backward:

var startsAfterNextLine = beforeIndex + 1 < subtitles.Count &&
                          subtitles[beforeIndex + 1].StartTime.TotalMilliseconds < firstPastedStart;

Paste a cue starting at 20s after a line ending at 10s, with the following line starting at 12s, and it lands at 10.5s rather than keeping its timestamp — LoadParagraphs applies the shift on != 0, so negative shifts apply too. PasteTimedAfterNextLineShiftsBackBetweenTheExistingLines shows this is deliberate and tested, not an accident.

It is a reasonable "insert here, in order" semantic, and arguably what a user pasting between two lines wants. But it is a user-visible change to what Ctrl+V does to timestamps, beyond "only shift when they would overlap", so it should be a decision rather than a side effect. If you want it, I would retitle the PR to say so.

@Ironship Ironship changed the title Grid paste: only shift pasted lines when they would overlap (SE4 parity) Grid paste: overlap-only shifting and insert-in-order re-timing (SE4 parity) Aug 7, 2026
@Ironship

Ironship commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Decision recorded: the insert-between-lines re-timing stays, and the PR is retitled to say so — "Grid paste: overlap-only shifting and insert-in-order re-timing (SE4 parity)".

The startsAfterNextLine branch is deliberate: pasting a block that begins after the existing next line into the gap would otherwise leave it out of order (the new rows would sort after the line that already follows the insertion point). Moving it back to lastEnd + MinimumBetweenLines preserves document order, which is what PasteTimedAfterNextLineShiftsBackBetweenTheExistingLines pins down. I kept it because it is the same "insert here, in order" behavior the other paste paths already have, and the PR description documents it explicitly alongside the overlap-only shift.

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.

Questions about 'Split line' behavior and pasting onto the grid in v5

3 participants