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()