Files
DeepSeek-Reasonix/internal/cli/diffview.go
T
YHH 4fbbca6a99 refactor(go): adopt modern stdlib idioms and gate them (#7831)
* refactor(go): adopt modern stdlib idioms across kernel and desktop

Mechanical sweep with golangci-lint's modernize/intrange/copyloopvar/
usestdlibvars fixers plus hand-fixes for what they could not rewrite:
slices.Contains, min/max, slices.Backward, WaitGroup.Go, reflect.TypeFor,
maps.Copy, strings.Cut/CutPrefix/SplitSeq, strings.Builder, range-over-int.
Net -549 lines with no behavior change.

Three loops in session.go stay index-based: slices.Backward yields element
copies, so the fixer's rewrite silently dropped writes to DecisionReceipts
and ToolCalls (caught by TestDelivery* and TestDependentSameBatch*). All 49
slices.Backward sites were audited for that shape; only these three write
through the index, and they carry a nolint with the reason.

CaptureAfter's scan loop had an empty body and never ran any logic, so the
fixer could not express it. Drop it and the accessor it was the only caller
of. omitzero is deliberately untouched: turning omitempty into omitzero
changes the JSON written for nested structs, which is a wire-format change
to session files and the extension protocol.

* build(lint): gate the modern-idiom linters

Nothing stopped the codebase from drifting back to pre-1.22 idioms. Both
modules are now clean for modernize, intrange, copyloopvar, and
usestdlibvars, so enable them rather than leaving the sweep as a one-off.

omitzero stays disabled with the reason inline; usetesting, perfsprint, and
errorlint still have residue and land separately.
2026-08-07 15:30:33 +08:00

269 lines
7.0 KiB
Go

// Renders a unified diff as line-numbered, syntax-highlighted rows on
// green/red background bars with a +/- gutter.
package cli
import (
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
"github.com/charmbracelet/x/ansi"
"reasonix/internal/event"
"reasonix/internal/i18n"
)
const tabWidth = 4
const (
// diffFoldLimit is the max lines to show in a diff when folding is enabled
// (/diff-fold toggle). 0 means show all lines.
diffFoldLimit = 40
bgDiffAdd = "\033[48;5;22m"
bgDiffDel = "\033[48;5;52m"
fgDiffAdd = "\033[1;38;5;46m"
fgDiffDel = "\033[1;38;5;203m"
)
var (
diffChromaFmt = formatters.Get("terminal256")
hunkRE = regexp.MustCompile(`^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@`)
)
// Resolve on each render so runtime theme switches and theme-sweep preview
// frames cannot retain syntax colours from the previous light/dark mode.
func activeDiffChromaStyle() *chroma.Style {
mode := chroma.Dark
if activeCLITheme.name == "light" {
mode = chroma.Light
}
return styles.GetForMode("github-dark", mode)
}
// diffStat renders a change's "+A -B" tally, green/red, omitting a zero side.
func diffStat(d event.FileDiff) string {
parts := make([]string, 0, 2)
if d.Added > 0 {
parts = append(parts, green("+"+strconv.Itoa(d.Added)))
}
if d.Removed > 0 {
parts = append(parts, red("-"+strconv.Itoa(d.Removed)))
}
return strings.Join(parts, " ")
}
func diffPath(args string) string {
var p struct {
Path string `json:"path"`
}
_ = json.Unmarshal([]byte(args), &p)
return p.Path
}
// diffBlock renders a writer call as a header line ("✎ name path +A -B") plus
// the highlighted, folded diff body. Returns nil when there's no textual diff.
func diffBlock(name, args string, d event.FileDiff, width, maxLines int) []string {
if d.Diff == "" {
return nil
}
path := diffPath(args)
header := " " + toolDot(name) + " " + toolHead(name, path, width)
if stat := diffStat(d); stat != "" {
header += " " + stat
}
return append([]string{header}, diffBody(d, path, width, maxLines)...)
}
// diffBody renders the hunks with a line-number gutter, dropping the file and
// "@@" headers (a dim "⋮" marks each hunk jump) and folding past maxLines to a
// "+N more" footer. path selects the syntax lexer.
func diffBody(d event.FileDiff, path string, width, maxLines int) []string {
if d.Diff == "" {
return nil
}
src := strings.Split(strings.TrimRight(d.Diff, "\n"), "\n")
// Drop the "--- a/… / +++ b/…" header pair positionally — matching the prefix
// on every line would eat real content (a deleted SQL "-- x" renders "--- x",
// an added "++ y" renders "+++ y").
if len(src) >= 2 && strings.HasPrefix(src[0], "--- ") && strings.HasPrefix(src[1], "+++ ") {
src = src[2:]
}
gw := gutterWidth(src)
var rows []string
oldNo, newNo, hunks := 0, 0, 0
for _, ln := range src {
if ln == "" {
continue
}
switch ln[0] {
case '@':
if m := hunkRE.FindStringSubmatch(ln); m != nil {
oldNo, newNo = atoi(m[1]), atoi(m[3])
}
if hunks > 0 {
rows = append(rows, " "+dim("⋮"))
}
hunks++
case '+':
rows = append(rows, diffBar('+', ln[1:], path, width, bgSGR(activeCLITheme.diffAddBG), fgSGR(activeCLITheme.success), newNo, gw))
newNo++
case '-':
rows = append(rows, diffBar('-', ln[1:], path, width, bgSGR(activeCLITheme.diffDelBG), fgSGR(activeCLITheme.err), oldNo, gw))
oldNo++
case '\\':
rows = append(rows, " "+dim(clampPlain(ln, width-2)))
default:
code := ln
if ln[0] == ' ' {
code = ln[1:]
}
rows = append(rows, diffContext(code, path, width, newNo, gw))
oldNo++
newNo++
}
}
if maxLines > 0 && len(rows) > maxLines {
folded := len(rows) - (maxLines - 1)
rows = rows[:maxLines-1]
rows = append(rows, " "+dim(fmt.Sprintf(i18n.M.DiffFoldedFmt, folded)))
}
return rows
}
// diffBar draws one added/removed row on a full-width coloured background. The
// bg is re-applied after every chroma reset — \033[0m would otherwise end the
// bar mid-line — and padded to the bar width so it runs edge to edge.
func diffBar(sign byte, code, path string, width int, bg, signFg string, lineNo, gw int) string {
gutter := dim(lpad(strconv.Itoa(lineNo), gw))
barW := max(width-2-gw-1, 4)
code = clampPlain(code, barW-2)
if !colorOn() {
return " " + gutter + " " + string(sign) + " " + code
}
hl := reapplyBG(highlightCode(path, code), bg)
pad := max(barW-2-visibleWidth(code), 0)
return " " + gutter + " " + bg + signFg + string(sign) + ansiReset + bg + " " + hl + strings.Repeat(" ", pad) + ansiReset
}
// diffContext draws an unchanged line: the gutter, no background, code aligned
// under the +/- rows' code column.
func diffContext(code, path string, width, lineNo, gw int) string {
gutter := dim(lpad(strconv.Itoa(lineNo), gw))
return " " + gutter + " " + highlightClamped(code, path, width-4-gw)
}
func gutterWidth(lines []string) int {
max := 0
for _, ln := range lines {
m := hunkRE.FindStringSubmatch(ln)
if m == nil {
continue
}
for _, p := range [][2]int{{1, 2}, {3, 4}} {
end := atoi(m[p[0]])
if m[p[1]] != "" {
end += atoi(m[p[1]])
} else {
end++
}
if end > max {
max = end
}
}
}
if w := len(strconv.Itoa(max)); w > 2 {
return w
}
return 2
}
func lpad(s string, w int) string {
if len(s) >= w {
return s
}
return strings.Repeat(" ", w-len(s)) + s
}
func atoi(s string) int {
n, _ := strconv.Atoi(s)
return n
}
func highlightClamped(code, path string, w int) string {
c := clampPlain(code, w)
if !colorOn() {
return c
}
return highlightCode(path, c)
}
func clampPlain(s string, w int) string {
if w < 1 {
w = 1
}
return ansi.Truncate(expandTabs(s), w, "")
}
// expandTabs replaces tabs with spaces to the next tabWidth stop. A literal tab
// has zero StringWidth but the terminal advances it to a tab stop, so leaving
// tabs in a background-bar row overflows the bar — expand them so the measured
// width matches what's drawn.
func expandTabs(s string) string {
if !strings.ContainsRune(s, '\t') {
return s
}
var b strings.Builder
col := 0
for _, r := range s {
if r == '\t' {
n := tabWidth - col%tabWidth
for range n {
b.WriteByte(' ')
}
col += n
continue
}
b.WriteRune(r)
col++
}
return b.String()
}
func reapplyBG(s, bg string) string {
if s == "" {
return s
}
return strings.ReplaceAll(s, ansiReset, ansiReset+bg)
}
// highlightCode returns code with chroma ANSI foreground colours for the lexer
// matched by path (plain fallback for unknown types). It emits no background, so
// it composes onto a diff bar; the caller re-applies the bar background.
func highlightCode(path, code string) string {
if code == "" {
return code
}
lexer := lexers.Match(path)
if lexer == nil {
lexer = lexers.Fallback
}
it, err := lexer.Tokenise(nil, code)
if err != nil {
return code
}
var b strings.Builder
if diffChromaFmt.Format(&b, activeDiffChromaStyle(), it) != nil {
return code
}
return strings.TrimRight(b.String(), "\n")
}