admin 8dc496b626
Some checks failed
CI / test (push) Has been cancelled
Release / release (push) Failing after 4m36s
first commit
2026-03-08 15:40:34 +07:00

197 lines
4.7 KiB
Go

package tui
import (
"fmt"
"os"
"strings"
)
// DiffLineKind represents the type of a diff line.
type DiffLineKind int
const (
DiffContext DiffLineKind = iota
DiffAdded
DiffRemoved
)
// DiffLine is a single line in a unified diff.
type DiffLine struct {
Kind DiffLineKind
Content string
}
// readFileForDiff extracts a file path from tool args and reads its content.
func readFileForDiff(rawArgs map[string]any) string {
for _, key := range []string{"path", "file_path", "filename", "file"} {
if p, ok := rawArgs[key].(string); ok {
data, err := os.ReadFile(p)
if err != nil {
return ""
}
return string(data)
}
}
return ""
}
// computeDiff computes a line-level diff between before and after text.
// Returns nil if the texts are identical.
func computeDiff(before, after string) []DiffLine {
if before == after {
return nil
}
beforeLines := splitLines(before)
afterLines := splitLines(after)
lcs := lcsLines(beforeLines, afterLines)
var all []DiffLine
bi, ai, li := 0, 0, 0
for li < len(lcs) {
for bi < len(beforeLines) && beforeLines[bi] != lcs[li] {
all = append(all, DiffLine{DiffRemoved, beforeLines[bi]})
bi++
}
for ai < len(afterLines) && afterLines[ai] != lcs[li] {
all = append(all, DiffLine{DiffAdded, afterLines[ai]})
ai++
}
all = append(all, DiffLine{DiffContext, lcs[li]})
bi++
ai++
li++
}
for bi < len(beforeLines) {
all = append(all, DiffLine{DiffRemoved, beforeLines[bi]})
bi++
}
for ai < len(afterLines) {
all = append(all, DiffLine{DiffAdded, afterLines[ai]})
ai++
}
return filterContext(all, 3)
}
// renderDiff renders diff lines with styles, capping output at maxLines.
func renderDiff(lines []DiffLine, styles Styles, maxLines int) string {
if len(lines) == 0 {
return ""
}
var b strings.Builder
displayed := 0
for _, line := range lines {
if maxLines > 0 && displayed >= maxLines {
b.WriteString(styles.DiffHeader.Render(fmt.Sprintf(" ... %d more lines", len(lines)-displayed)))
b.WriteString("\n")
break
}
switch line.Kind {
case DiffAdded:
b.WriteString(styles.DiffAdded.Render("+ " + line.Content))
case DiffRemoved:
b.WriteString(styles.DiffRemoved.Render("- " + line.Content))
case DiffContext:
b.WriteString(styles.DiffContext.Render(" " + line.Content))
}
b.WriteString("\n")
displayed++
}
return b.String()
}
// lcsLines computes the longest common subsequence of two string slices.
func lcsLines(a, b []string) []string {
m, n := len(a), len(b)
if m == 0 || n == 0 {
return nil
}
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if a[i-1] == b[j-1] {
dp[i][j] = dp[i-1][j-1] + 1
} else if dp[i-1][j] >= dp[i][j-1] {
dp[i][j] = dp[i-1][j]
} else {
dp[i][j] = dp[i][j-1]
}
}
}
result := make([]string, dp[m][n])
k := dp[m][n] - 1
i, j := m, n
for i > 0 && j > 0 {
if a[i-1] == b[j-1] {
result[k] = a[i-1]
k--
i--
j--
} else if dp[i-1][j] >= dp[i][j-1] {
i--
} else {
j--
}
}
return result
}
// filterContext keeps only diff lines near changes, with contextLines of context.
func filterContext(lines []DiffLine, contextLines int) []DiffLine {
if len(lines) == 0 {
return nil
}
keep := make([]bool, len(lines))
for i, line := range lines {
if line.Kind != DiffContext {
lo := i - contextLines
if lo < 0 {
lo = 0
}
hi := i + contextLines
if hi >= len(lines) {
hi = len(lines) - 1
}
for j := lo; j <= hi; j++ {
keep[j] = true
}
}
}
var result []DiffLine
for i, line := range lines {
if keep[i] {
result = append(result, line)
}
}
return result
}
// splitLines splits text into lines, removing a trailing empty line from a trailing newline.
func splitLines(s string) []string {
if s == "" {
return nil
}
lines := strings.Split(s, "\n")
if len(lines) > 0 && lines[len(lines)-1] == "" {
lines = lines[:len(lines)-1]
}
return lines
}