1
2 package format
3
4 import (
5 "bytes"
6 "fmt"
7 "strings"
8 "unicode"
9
10 "gotest.tools/v3/internal/difflib"
11 )
12
13 const (
14 contextLines = 2
15 )
16
17
18 type DiffConfig struct {
19 A string
20 B string
21 From string
22 To string
23 }
24
25
26
27 func UnifiedDiff(conf DiffConfig) string {
28 a := strings.SplitAfter(conf.A, "\n")
29 b := strings.SplitAfter(conf.B, "\n")
30 groups := difflib.NewMatcher(a, b).GetGroupedOpCodes(contextLines)
31 if len(groups) == 0 {
32 return ""
33 }
34
35 buf := new(bytes.Buffer)
36 writeFormat := func(format string, args ...interface{}) {
37 buf.WriteString(fmt.Sprintf(format, args...))
38 }
39 writeLine := func(prefix string, s string) {
40 buf.WriteString(prefix + s)
41 }
42 if hasWhitespaceDiffLines(groups, a, b) {
43 writeLine = visibleWhitespaceLine(writeLine)
44 }
45 formatHeader(writeFormat, conf)
46 for _, group := range groups {
47 formatRangeLine(writeFormat, group)
48 for _, opCode := range group {
49 in, out := a[opCode.I1:opCode.I2], b[opCode.J1:opCode.J2]
50 switch opCode.Tag {
51 case 'e':
52 formatLines(writeLine, " ", in)
53 case 'r':
54 formatLines(writeLine, "-", in)
55 formatLines(writeLine, "+", out)
56 case 'd':
57 formatLines(writeLine, "-", in)
58 case 'i':
59 formatLines(writeLine, "+", out)
60 }
61 }
62 }
63 return buf.String()
64 }
65
66
67
68 func hasWhitespaceDiffLines(groups [][]difflib.OpCode, a, b []string) bool {
69 for _, group := range groups {
70 in, out := new(bytes.Buffer), new(bytes.Buffer)
71 for _, opCode := range group {
72 if opCode.Tag == 'e' {
73 continue
74 }
75 for _, line := range a[opCode.I1:opCode.I2] {
76 in.WriteString(line)
77 }
78 for _, line := range b[opCode.J1:opCode.J2] {
79 out.WriteString(line)
80 }
81 }
82 if removeWhitespace(in.String()) == removeWhitespace(out.String()) {
83 return true
84 }
85 }
86 return false
87 }
88
89 func removeWhitespace(s string) string {
90 var result []rune
91 for _, r := range s {
92 if !unicode.IsSpace(r) {
93 result = append(result, r)
94 }
95 }
96 return string(result)
97 }
98
99 func visibleWhitespaceLine(ws func(string, string)) func(string, string) {
100 mapToVisibleSpace := func(r rune) rune {
101 switch r {
102 case '\n':
103 case ' ':
104 return '·'
105 case '\t':
106 return '▷'
107 case '\v':
108 return '▽'
109 case '\r':
110 return '↵'
111 case '\f':
112 return '↓'
113 default:
114 if unicode.IsSpace(r) {
115 return '�'
116 }
117 }
118 return r
119 }
120 return func(prefix, s string) {
121 ws(prefix, strings.Map(mapToVisibleSpace, s))
122 }
123 }
124
125 func formatHeader(wf func(string, ...interface{}), conf DiffConfig) {
126 if conf.From != "" || conf.To != "" {
127 wf("--- %s\n", conf.From)
128 wf("+++ %s\n", conf.To)
129 }
130 }
131
132 func formatRangeLine(wf func(string, ...interface{}), group []difflib.OpCode) {
133 first, last := group[0], group[len(group)-1]
134 range1 := formatRangeUnified(first.I1, last.I2)
135 range2 := formatRangeUnified(first.J1, last.J2)
136 wf("@@ -%s +%s @@\n", range1, range2)
137 }
138
139
140 func formatRangeUnified(start, stop int) string {
141
142 beginning := start + 1
143 length := stop - start
144 if length == 1 {
145 return fmt.Sprintf("%d", beginning)
146 }
147 if length == 0 {
148 beginning--
149 }
150 return fmt.Sprintf("%d,%d", beginning, length)
151 }
152
153 func formatLines(writeLine func(string, string), prefix string, lines []string) {
154 for _, line := range lines {
155 writeLine(prefix, line)
156 }
157
158
159 if !strings.HasSuffix(lines[len(lines)-1], "\n") {
160 writeLine("", "\n")
161 }
162 }
163
View as plain text