162 lines
3.7 KiB
Go
162 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")
|
|||
|
}
|
|||
|
}
|