xerrors: gobble colon and newline in edge cases

We gobble newlines at the start of a detailed
section, at end before a new message, but not
at the end of a message. This CL add that.

Also, we should not print the colon after the last
non-detailed message if the detailed message
consists solely of newlines that are gobbled.

Change-Id: I9888253e600fa1d7a05dd61f2cc49cda4275ceb1
Reviewed-on: https://go-review.googlesource.com/c/160717
Reviewed-by: Damien Neil <dneil@google.com>
Run-TryBot: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/adaptor.go b/adaptor.go
index 83a7945..4317f24 100644
--- a/adaptor.go
+++ b/adaptor.go
@@ -70,8 +70,6 @@
 
 loop:
 	for {
-		p.inDetail = false
-
 		switch v := err.(type) {
 		case Formatter:
 			err = v.FormatError((*printer)(p))
@@ -85,15 +83,13 @@
 		if err == nil {
 			break
 		}
-		if !p.inDetail || !p.printDetail {
+		if p.needColon || !p.printDetail {
 			p.buf.WriteByte(':')
-		}
-		// Strip last newline of detail.
-		if bytes.HasSuffix(p.buf.Bytes(), detailSep) {
-			p.buf.Truncate(p.buf.Len() - len(detailSep))
+			p.needColon = false
 		}
 		p.buf.WriteString(sep)
 		p.inDetail = false
+		p.needNewline = false
 	}
 
 exit:
@@ -135,6 +131,7 @@
 
 	printDetail bool
 	inDetail    bool
+	needColon   bool
 	needNewline bool
 }
 
@@ -143,24 +140,32 @@
 		if len(b) == 0 {
 			return 0, nil
 		}
-		if s.inDetail && s.needNewline {
-			s.needNewline = false
-			s.buf.WriteByte(':')
-			s.buf.Write(detailSep)
+		if s.inDetail && s.needColon {
+			s.needNewline = true
 			if b[0] == '\n' {
 				b = b[1:]
 			}
 		}
 		k := 0
 		for i, c := range b {
+			if s.needNewline {
+				if s.inDetail && s.needColon {
+					s.buf.WriteByte(':')
+					s.needColon = false
+				}
+				s.buf.Write(detailSep)
+				s.needNewline = false
+			}
 			if c == '\n' {
 				s.buf.Write(b[k:i])
-				s.buf.Write(detailSep)
 				k = i + 1
+				s.needNewline = true
 			}
 		}
 		s.buf.Write(b[k:])
-		s.needNewline = !s.inDetail
+		if !s.inDetail {
+			s.needColon = true
+		}
 	} else if !s.inDetail {
 		s.buf.Write(b)
 	}
diff --git a/fmt_test.go b/fmt_test.go
index 8534c29..6744b8a 100644
--- a/fmt_test.go
+++ b/fmt_test.go
@@ -132,6 +132,26 @@
 			"\n    and the 12 monkeys" +
 			"\n    are laughing",
 	}, {
+		err:  &oneNewline{nil},
+		fmt:  "%+v",
+		want: "123",
+	}, {
+		err: &oneNewline{&oneNewline{nil}},
+		fmt: "%+v",
+		want: "123:" +
+			"\n  - 123",
+	}, {
+		err:  &newlineAtEnd{nil},
+		fmt:  "%+v",
+		want: "newlineAtEnd:\n    detail",
+	}, {
+		err: &newlineAtEnd{&newlineAtEnd{nil}},
+		fmt: "%+v",
+		want: "newlineAtEnd:" +
+			"\n    detail" +
+			"\n  - newlineAtEnd:" +
+			"\n    detail",
+	}, {
 		err: framed,
 		fmt: "%+v",
 		want: "something:" +
@@ -302,7 +322,7 @@
 			var ok bool
 			if tc.regexp {
 				var err error
-				ok, err = regexp.MatchString(tc.want, got)
+				ok, err = regexp.MatchString(tc.want+"$", got)
 				if err != nil {
 					t.Fatal(err)
 				}
@@ -420,6 +440,42 @@
 	return nil
 }
 
+type oneNewline struct {
+	next error
+}
+
+func (e *oneNewline) Error() string { return fmt.Sprint(e) }
+
+func (e *oneNewline) Format(s fmt.State, verb rune) {
+	xerrors.FormatError(e, s, verb)
+}
+
+func (e *oneNewline) FormatError(p xerrors.Printer) (next error) {
+	p.Print("1")
+	p.Print("2")
+	p.Print("3")
+	p.Detail()
+	p.Print("\n")
+	return e.next
+}
+
+type newlineAtEnd struct {
+	next error
+}
+
+func (e *newlineAtEnd) Error() string { return fmt.Sprint(e) }
+
+func (e *newlineAtEnd) Format(s fmt.State, verb rune) {
+	xerrors.FormatError(e, s, verb)
+}
+
+func (e *newlineAtEnd) FormatError(p xerrors.Printer) (next error) {
+	p.Print("newlineAtEnd")
+	p.Detail()
+	p.Print("detail\n")
+	return e.next
+}
+
 type adapted struct {
 	msg string
 	err error