Skip to content

Format-on-save moves the cursor to a wrong/random position (needs diff-based cursor mapping) #2777

Description

@sinelaw

Split off from #2706 as a tracked follow-up.

Problem

When an on-save action rewrites the buffer (format-on-save, or trim-trailing-whitespace-on-save), the cursor is not mapped through the content change, so it lands on the wrong text — and non-deterministically.

Repro (from #2706): un-indented Rust file, cursor at end of line 3 (let b = 2;, Ln 3, Col 11), format-on-save prepends 4 spaces. Expected the cursor to track its text (end at Ln 3, Col 15, or at worst stay at Col 11); observed it landing inside the word let (Ln 3, Col 7), and a separate run from the same start landed on a different line (Ln 4, Col 3). Same input, different landing spot.

The other on-save defects (the false "File changed on disk" prompt from stale on-disk state, and the spurious "Reverted to saved file" that reset CRLF→LF) are handled separately in #2711 / the on-save-state PR — this issue is only the cursor mapping.

Proposed approach

Map the cursor through a diff of the pre-transform buffer vs. the formatter output, in the core on-save path (crates/fresh-editor/src/app/on_save_actions.rs / app/file_operations.rs) — not a plugin, since it repositions core cursor/selection state.

  • Reuse the existing internal diff crates/fresh-editor/src/model/line_diff.rs (diff_lines, LCS-based). No git and no new dependency — it's a pure in-memory old-vs-new text diff.
  • Line diff first, then character-refine only the changed line(s); map the cursor's old offset through equal/changed regions, snapping to the end of the longest common prefix when it falls inside a changed hunk.
  • Also map any active selection endpoints the same way.

Performance / huge files

The current line_diff is an O(N·M) DP. To keep it cheap regardless of file size:

  1. Trim the common prefix/suffix before diffing (formatter output ≈ input, so this collapses the problem to the changed span).
  2. Line-level first; char-refine only changed lines.
  3. A size/complexity cap with graceful fallback to a cheap line-anchored reposition (or clamp) so the editor never stalls on pathological input.

(Optional future: swap the DP for a Myers O(N·D) diff to remove the caveat entirely.)

Acceptance

  • After format-on-save / trim-on-save, the cursor stays anchored to the same logical text (the Format On Save #2706 repro lands at the end of let b = 2;), deterministically.
  • Selections are preserved analogously.
  • No measurable stall on large files (prefix/suffix trim + fallback).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions