| 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") |
| } |
| } |