161 lines
3.7 KiB
Go
161 lines
3.7 KiB
Go
package format
|
||
|
||
import (
|
||
"bytes"
|
||
"fmt"
|
||
"strings"
|
||
"unicode"
|
||
|
||
"gotest.tools/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 '<27>'
|
||
}
|
||
}
|
||
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")
|
||
}
|
||
}
|