Use triple-quote formatting for multiline strings (#229)

For strings, []bytes containing text data, Error method output, and
String method output, use the triple-quoted syntax.
This improves readability by presenting the data more naturally
compared to a single-line quoted string with many escaped characters.
diff --git a/cmp/compare_test.go b/cmp/compare_test.go
index b34530b..ba39bde 100644
--- a/cmp/compare_test.go
+++ b/cmp/compare_test.go
@@ -1134,6 +1134,19 @@
 		wantEqual: false,
 		reason:    "avoid triple-quote syntax due to visual equivalence of differences",
 	}, {
+		label: label + "/TripleQuoteStringer",
+		x: []fmt.Stringer{
+			bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hello, playground\")\n}\n")),
+			bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n)\n\nfunc main() {\n\tfmt.Println(\"My favorite number is\", rand.Intn(10))\n}\n")),
+		},
+		y: []fmt.Stringer{
+			bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hello, playground\")\n}\n")),
+			bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n\t\"math\"\n)\n\nfunc main() {\n\tfmt.Printf(\"Now you have %g problems.\\n\", math.Sqrt(7))\n}\n")),
+		},
+		opts:      []cmp.Option{cmp.Comparer(func(x, y fmt.Stringer) bool { return x.String() == y.String() })},
+		wantEqual: false,
+		reason:    "multi-line String output should be formatted with triple quote",
+	}, {
 		label:     label + "/LimitMaximumBytesDiffs",
 		x:         []byte("\xcd====\x06\x1f\xc2\xcc\xc2-S=====\x1d\xdfa\xae\x98\x9fH======ǰ\xb7=======\xef====:\\\x94\xe6J\xc7=====\xb4======\n\n\xf7\x94===========\xf2\x9c\xc0f=====4\xf6\xf1\xc3\x17\x82======n\x16`\x91D\xc6\x06=======\x1cE====.===========\xc4\x18=======\x8a\x8d\x0e====\x87\xb1\xa5\x8e\xc3=====z\x0f1\xaeU======G,=======5\xe75\xee\x82\xf4\xce====\x11r===========\xaf]=======z\x05\xb3\x91\x88%\xd2====\n1\x89=====i\xb7\x055\xe6\x81\xd2=============\x883=@̾====\x14\x05\x96%^t\x04=====\xe7Ȉ\x90\x1d============="),
 		y:         []byte("\\====|\x96\xe7SB\xa0\xab=====\xf0\xbd\xa5q\xab\x17;======\xabP\x00=======\xeb====\xa5\x14\xe6O(\xe4=====(======/c@?===========\xd9x\xed\x13=====J\xfc\x918B\x8d======a8A\xebs\x04\xae=======\aC====\x1c===========\x91\"=======uؾ====s\xec\x845\a=====;\xabS9t======\x1f\x1b=======\x80\xab/\xed+:;====\xeaI===========\xabl=======\xb9\xe9\xfdH\x93\x8e\u007f====ח\xe5=====Ig\x88m\xf5\x01V=============\xf7+4\xb0\x92E====\x9fj\xf8&\xd0h\xf9=====\xeeΨ\r\xbf============="),
diff --git a/cmp/report_reflect.go b/cmp/report_reflect.go
index 28e0e92..786f671 100644
--- a/cmp/report_reflect.go
+++ b/cmp/report_reflect.go
@@ -5,6 +5,7 @@
 package cmp
 
 import (
+	"bytes"
 	"fmt"
 	"reflect"
 	"strconv"
@@ -138,14 +139,7 @@
 				}
 			}()
 			if prefix != "" {
-				maxLen := len(strVal)
-				if opts.LimitVerbosity {
-					maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc...
-				}
-				if len(strVal) > maxLen+len(textEllipsis) {
-					return textLine(prefix + formatString(strVal[:maxLen]) + string(textEllipsis))
-				}
-				return textLine(prefix + formatString(strVal))
+				return opts.formatString(prefix, strVal)
 			}
 		}
 	}
@@ -177,14 +171,7 @@
 	case reflect.Complex64, reflect.Complex128:
 		return textLine(fmt.Sprint(v.Complex()))
 	case reflect.String:
-		maxLen := v.Len()
-		if opts.LimitVerbosity {
-			maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc...
-		}
-		if v.Len() > maxLen+len(textEllipsis) {
-			return textLine(formatString(v.String()[:maxLen]) + string(textEllipsis))
-		}
-		return textLine(formatString(v.String()))
+		return opts.formatString("", v.String())
 	case reflect.UnsafePointer, reflect.Chan, reflect.Func:
 		return textLine(formatPointer(value.PointerOf(v), true))
 	case reflect.Struct:
@@ -216,6 +203,17 @@
 		if v.IsNil() {
 			return textNil
 		}
+
+		// Check whether this is a []byte of text data.
+		if t.Elem() == reflect.TypeOf(byte(0)) {
+			b := v.Bytes()
+			isPrintSpace := func(r rune) bool { return unicode.IsPrint(r) && unicode.IsSpace(r) }
+			if len(b) > 0 && utf8.Valid(b) && len(bytes.TrimFunc(b, isPrintSpace)) == 0 {
+				out = opts.formatString("", string(b))
+				return opts.WithTypeMode(emitType).FormatType(t, out)
+			}
+		}
+
 		fallthrough
 	case reflect.Array:
 		maxLen := v.Len()
@@ -301,6 +299,49 @@
 	}
 }
 
+func (opts formatOptions) formatString(prefix, s string) textNode {
+	maxLen := len(s)
+	maxLines := strings.Count(s, "\n") + 1
+	if opts.LimitVerbosity {
+		maxLen = (1 << opts.verbosity()) << 5   // 32, 64, 128, 256, etc...
+		maxLines = (1 << opts.verbosity()) << 2 //  4, 8, 16, 32, 64, etc...
+	}
+
+	// For multiline strings, use the triple-quote syntax,
+	// but only use it when printing removed or inserted nodes since
+	// we only want the extra verbosity for those cases.
+	lines := strings.Split(strings.TrimSuffix(s, "\n"), "\n")
+	isTripleQuoted := len(lines) >= 4 && (opts.DiffMode == '-' || opts.DiffMode == '+')
+	for i := 0; i < len(lines) && isTripleQuoted; i++ {
+		lines[i] = strings.TrimPrefix(strings.TrimSuffix(lines[i], "\r"), "\r") // trim leading/trailing carriage returns for legacy Windows endline support
+		isPrintable := func(r rune) bool {
+			return unicode.IsPrint(r) || r == '\t' // specially treat tab as printable
+		}
+		line := lines[i]
+		isTripleQuoted = !strings.HasPrefix(strings.TrimPrefix(line, prefix), `"""`) && !strings.HasPrefix(line, "...") && strings.TrimFunc(line, isPrintable) == "" && len(line) <= maxLen
+	}
+	if isTripleQuoted {
+		var list textList
+		list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(prefix + `"""`), ElideComma: true})
+		for i, line := range lines {
+			if numElided := len(lines) - i; i == maxLines-1 && numElided > 1 {
+				comment := commentString(fmt.Sprintf("%d elided lines", numElided))
+				list = append(list, textRecord{Diff: opts.DiffMode, Value: textEllipsis, ElideComma: true, Comment: comment})
+				break
+			}
+			list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(line), ElideComma: true})
+		}
+		list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(prefix + `"""`), ElideComma: true})
+		return &textWrap{Prefix: "(", Value: list, Suffix: ")"}
+	}
+
+	// Format the string as a single-line quoted string.
+	if len(s) > maxLen+len(textEllipsis) {
+		return textLine(prefix + formatString(s[:maxLen]) + string(textEllipsis))
+	}
+	return textLine(prefix + formatString(s))
+}
+
 // formatMapKey formats v as if it were a map key.
 // The result is guaranteed to be a single line.
 func formatMapKey(v reflect.Value, disambiguate bool, ptrs *pointerReferences) string {
diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs
index 05fa3fd..dee035d 100644
--- a/cmp/testdata/diffs
+++ b/cmp/testdata/diffs
@@ -730,6 +730,39 @@
   	... // 7 identical lines
   }, "\n")
 >>> TestDiff/Reporter/AvoidTripleQuoteIdenticalWhitespace
+<<< TestDiff/Reporter/TripleQuoteStringer
+  []fmt.Stringer{
+  	s"package main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hel"...,
+- 	(
+- 		s"""
+- 		package main
+- 		
+- 		import (
+- 			"fmt"
+- 			"math/rand"
+- 		)
+- 		
+- 		func main() {
+- 			fmt.Println("My favorite number is", rand.Intn(10))
+- 		}
+- 		s"""
+- 	),
++ 	(
++ 		s"""
++ 		package main
++ 		
++ 		import (
++ 			"fmt"
++ 			"math"
++ 		)
++ 		
++ 		func main() {
++ 			fmt.Printf("Now you have %g problems.\n", math.Sqrt(7))
++ 		}
++ 		s"""
++ 	),
+  }
+>>> TestDiff/Reporter/TripleQuoteStringer
 <<< TestDiff/Reporter/LimitMaximumBytesDiffs
   []uint8{
 - 	0xcd, 0x3d, 0x3d, 0x3d, 0x3d, 0x06, 0x1f, 0xc2, 0xcc, 0xc2, 0x2d, 0x53,                         // -|.====.....-S|