package format import ( "bytes" "fmt" "strings" "unicode" "gotest.tools/v3/internal/difflib" ) const ( contextLines = 2 ) // DiffConfig for a unified diff type DiffConfig struct { A string B string From string To string } // UnifiedDiff is a modified version of difflib.WriteUnifiedDiff with better // support for showing the whitespace differences. func UnifiedDiff(conf DiffConfig) string { a := strings.SplitAfter(conf.A, "\n") b := strings.SplitAfter(conf.B, "\n") groups := difflib.NewMatcher(a, b).GetGroupedOpCodes(contextLines) if len(groups) == 0 { return "" } buf := new(bytes.Buffer) writeFormat := func(format string, args ...interface{}) { buf.WriteString(fmt.Sprintf(format, args...)) } writeLine := func(prefix string, s string) { buf.WriteString(prefix + s) } if hasWhitespaceDiffLines(groups, a, b) { writeLine = visibleWhitespaceLine(writeLine) } formatHeader(writeFormat, conf) for _, group := range groups { formatRangeLine(writeFormat, group) for _, opCode := range group { in, out := a[opCode.I1:opCode.I2], b[opCode.J1:opCode.J2] switch opCode.Tag { case 'e': formatLines(writeLine, " ", in) case 'r': formatLines(writeLine, "-", in) formatLines(writeLine, "+", out) case 'd': formatLines(writeLine, "-", in) case 'i': formatLines(writeLine, "+", out) } } } return buf.String() } // hasWhitespaceDiffLines returns true if any diff groups is only different // because of whitespace characters. func hasWhitespaceDiffLines(groups [][]difflib.OpCode, a, b []string) bool { for _, group := range groups { in, out := new(bytes.Buffer), new(bytes.Buffer) for _, opCode := range group { if opCode.Tag == 'e' { continue } for _, line := range a[opCode.I1:opCode.I2] { in.WriteString(line) } for _, line := range b[opCode.J1:opCode.J2] { out.WriteString(line) } } if removeWhitespace(in.String()) == removeWhitespace(out.String()) { return true } } return false } func removeWhitespace(s string) string { var result []rune for _, r := range s { if !unicode.IsSpace(r) { result = append(result, r) } } return string(result) } func visibleWhitespaceLine(ws func(string, string)) func(string, string) { mapToVisibleSpace := func(r rune) rune { switch r { case '\n': case ' ': return '·' case '\t': return '▷' case '\v': return '▽' case '\r': return '↵' case '\f': return '↓' default: if unicode.IsSpace(r) { return '�' } } return r } return func(prefix, s string) { ws(prefix, strings.Map(mapToVisibleSpace, s)) } } func formatHeader(wf func(string, ...interface{}), conf DiffConfig) { if conf.From != "" || conf.To != "" { wf("--- %s\n", conf.From) wf("+++ %s\n", conf.To) } } func formatRangeLine(wf func(string, ...interface{}), group []difflib.OpCode) { first, last := group[0], group[len(group)-1] range1 := formatRangeUnified(first.I1, last.I2) range2 := formatRangeUnified(first.J1, last.J2) wf("@@ -%s +%s @@\n", range1, range2) } // Convert range to the "ed" format func formatRangeUnified(start, stop int) string { // Per the diff spec at http://www.unix.org/single_unix_specification/ beginning := start + 1 // lines start numbering with one length := stop - start if length == 1 { return fmt.Sprintf("%d", beginning) } if length == 0 { beginning-- // empty ranges begin at line just before the range } return fmt.Sprintf("%d,%d", beginning, length) } func formatLines(writeLine func(string, string), prefix string, lines []string) { for _, line := range lines { writeLine(prefix, line) } // Add a newline if the last line is missing one so that the diff displays // properly. if !strings.HasSuffix(lines[len(lines)-1], "\n") { writeLine("", "\n") } }