From c71313ea981db57199d6a4fef9ca28123913b6aa Mon Sep 17 00:00:00 2001 From: CnsMaple Date: Fri, 5 Jun 2026 15:55:37 +0800 Subject: [PATCH] fix(ui): normalize line endings when computing session file diff stats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sidebar's modified-files diff stats are computed by diffing the first and last versions of a file stored in history. When a file was first written with one set of line endings (e.g. CRLF, preserved by the edit tool from a Windows editor) and later overwritten with different line endings (e.g. LF, typical for LLM-generated content written by the write tool), every line in the file was reported as changed. A 1000-line file gained 1 line would show "+1001 -1000" instead of "+1 -0". Normalize both sides to LF before calling diff.GenerateDiff so the stats reflect the real textual delta. A regression test covers the CRLF-vs-LF mismatch and the identical-content case. 💘 Generated with Crush Assisted-by: Crush:MiniMax-M3 --- internal/ui/model/session.go | 9 ++++++- internal/ui/model/session_test.go | 44 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/internal/ui/model/session.go b/internal/ui/model/session.go index aa31009b89..3b42814c69 100644 --- a/internal/ui/model/session.go +++ b/internal/ui/model/session.go @@ -136,7 +136,14 @@ func (m *UI) loadSessionFiles(sessionID string) ([]SessionFile, error) { } } - _, additions, deletions := diff.GenerateDiff(first.Content, last.Content, first.Path) + // Normalize line endings so that a CRLF/LF mismatch between + // historical versions does not inflate the diff stats. Files + // round-tripped through the write tool may end up with different + // line endings than the original (the LLM typically emits LF), + // which previously caused every line to be reported as changed. + firstContent, _ := fsext.ToUnixLineEndings(first.Content) + lastContent, _ := fsext.ToUnixLineEndings(last.Content) + _, additions, deletions := diff.GenerateDiff(firstContent, lastContent, first.Path) sessionFiles = append(sessionFiles, SessionFile{ FirstVersion: first, diff --git a/internal/ui/model/session_test.go b/internal/ui/model/session_test.go index 7aa1a3f9f3..73d115c57b 100644 --- a/internal/ui/model/session_test.go +++ b/internal/ui/model/session_test.go @@ -1,10 +1,13 @@ package model import ( + "fmt" "strings" "testing" "charm.land/lipgloss/v2" + "github.com/charmbracelet/crush/internal/diff" + "github.com/charmbracelet/crush/internal/fsext" "github.com/charmbracelet/crush/internal/history" "github.com/charmbracelet/crush/internal/ui/styles" "github.com/stretchr/testify/require" @@ -111,6 +114,47 @@ func TestFileList(t *testing.T) { }) } +func TestLoadSessionFilesNormalizesLineEndings(t *testing.T) { + t.Parallel() + + makeContent := func(sep string) string { + var b strings.Builder + for i := 0; i < 1000; i++ { + b.WriteString(fmt.Sprintf("line %d", i)) + b.WriteString(sep) + } + return b.String() + } + + t.Run("CRLF vs LF reports the real delta, not the line ending swap", func(t *testing.T) { + t.Parallel() + + first := history.File{Path: "main.go", Content: makeContent("\r\n"), Version: 0} + last := history.File{Path: "main.go", Content: makeContent("\n") + "added\n", Version: 1} + + firstContent, _ := fsext.ToUnixLineEndings(first.Content) + lastContent, _ := fsext.ToUnixLineEndings(last.Content) + _, additions, removals := diff.GenerateDiff(firstContent, lastContent, first.Path) + + require.Equal(t, 1, additions, "expected one addition for the appended line, got %d", additions) + require.Equal(t, 0, removals, "expected zero removals, got %d", removals) + }) + + t.Run("identical content with mismatched line endings reports no changes", func(t *testing.T) { + t.Parallel() + + first := history.File{Path: "main.go", Content: makeContent("\r\n"), Version: 0} + last := history.File{Path: "main.go", Content: makeContent("\n"), Version: 1} + + firstContent, _ := fsext.ToUnixLineEndings(first.Content) + lastContent, _ := fsext.ToUnixLineEndings(last.Content) + _, additions, removals := diff.GenerateDiff(firstContent, lastContent, first.Path) + + require.Equal(t, 0, additions) + require.Equal(t, 0, removals) + }) +} + func minimalFileStyles() *styles.Styles { st := styles.CharmtonePantera() st.Files.Path = lipgloss.NewStyle()