internal/lsp: clean up formatting header handling

This is mostly to set things up to use golden files the same way for other commands.

Change-Id: I7fcc7165706763e655b0e46f0790b367fe5d3d59
Reviewed-on: https://go-review.googlesource.com/c/tools/+/174018
Run-TryBot: Ian Cottrell <iancottrell@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/cmd/cmd_test.go b/internal/lsp/cmd/cmd_test.go
index 3a5b45b..97eb94f 100644
--- a/internal/lsp/cmd/cmd_test.go
+++ b/internal/lsp/cmd/cmd_test.go
@@ -5,9 +5,13 @@
 package cmd_test
 
 import (
+	"bytes"
 	"io/ioutil"
 	"os"
+	"strings"
 	"testing"
+	"unicode"
+	"unicode/utf8"
 
 	"golang.org/x/tools/go/packages/packagestest"
 	"golang.org/x/tools/internal/lsp/cmd"
@@ -73,3 +77,65 @@
 	}
 	return string(data)
 }
+
+// normalizePaths replaces all paths present in s with just the fragment portion
+// this is used to make golden files not depend on the temporary paths of the files
+func (r *runner) normalizePaths(s string) string {
+	type entry struct {
+		path  string
+		index int
+	}
+	match := make([]entry, len(r.data.Exported.Modules))
+	// collect the initial state of all the matchers
+	for i, m := range r.data.Exported.Modules {
+		// any random file will do, we collect the first one only
+		for f := range m.Files {
+			path := strings.TrimSuffix(r.data.Exported.File(m.Name, f), f)
+			index := strings.Index(s, path)
+			match[i] = entry{path, index}
+			break
+		}
+	}
+	// result should be the same or shorter than the input
+	buf := bytes.NewBuffer(make([]byte, 0, len(s)))
+	last := 0
+	for {
+		// find the nearest path match to the start of the buffer
+		next := -1
+		nearest := len(s)
+		for i, c := range match {
+			if c.index >= 0 && nearest > c.index {
+				nearest = c.index
+				next = i
+			}
+		}
+		// if there are no matches, we copy the rest of the string and are done
+		if next < 0 {
+			buf.WriteString(s[last:])
+			return buf.String()
+		}
+		// we have a match
+		n := &match[next]
+		// copy up to the start of the match
+		buf.WriteString(s[last:n.index])
+		// skip over the non fragment prefix
+		last = n.index + len(n.path)
+		// now try to convert the fragment part
+		for last < len(s) {
+			r, size := utf8.DecodeRuneInString(s[last:])
+			if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '/' {
+				buf.WriteRune(r)
+			} else if r == '\\' {
+				buf.WriteRune('/')
+			} else {
+				break
+			}
+			last += size
+		}
+		// see what the next match for this path is
+		n.index = strings.Index(s[last:], n.path)
+		if n.index >= 0 {
+			n.index += last
+		}
+	}
+}
diff --git a/internal/lsp/cmd/format.go b/internal/lsp/cmd/format.go
index 02f9bc3..79403b0 100644
--- a/internal/lsp/cmd/format.go
+++ b/internal/lsp/cmd/format.go
@@ -97,7 +97,7 @@
 		}
 		if f.Diff {
 			printIt = false
-			u := diff.ToUnified(filename, filename, lines, ops)
+			u := diff.ToUnified(filename+".orig", filename, lines, ops)
 			fmt.Print(u)
 		}
 		if printIt {
diff --git a/internal/lsp/cmd/format_test.go b/internal/lsp/cmd/format_test.go
index 380c6c5..f95c24a 100644
--- a/internal/lsp/cmd/format_test.go
+++ b/internal/lsp/cmd/format_test.go
@@ -7,9 +7,9 @@
 import (
 	"bytes"
 	"context"
-	"fmt"
 	"io/ioutil"
 	"os/exec"
+	"regexp"
 	"strings"
 	"testing"
 
@@ -26,14 +26,7 @@
 func (r *runner) Format(t *testing.T, data tests.Formats) {
 	for _, spn := range data {
 		for _, mode := range formatModes {
-			isDiff := false
-			tag := "gofmt"
-			for _, arg := range mode {
-				tag += arg
-				if arg == "-d" {
-					isDiff = true
-				}
-			}
+			tag := "gofmt" + strings.Join(mode, "")
 			uri := spn.URI()
 			filename, err := uri.Filename()
 			if err != nil {
@@ -45,16 +38,7 @@
 				buf := &bytes.Buffer{}
 				cmd.Stdout = buf
 				cmd.Run() // ignore error, sometimes we have intentionally ungofmt-able files
-				contents := buf.String()
-				// strip the unwanted diff line
-				if isDiff {
-					if strings.HasPrefix(contents, "diff -u") {
-						if i := strings.IndexRune(contents, '\n'); i >= 0 && i < len(contents)-1 {
-							contents = contents[i+1:]
-						}
-					}
-					contents, _ = stripFileHeader(contents)
-				}
+				contents := r.normalizePaths(fixFileHeader(buf.String()))
 				return ioutil.WriteFile(golden, []byte(contents), 0666)
 			}))
 			if expect == "" {
@@ -66,13 +50,7 @@
 			got := captureStdOut(t, func() {
 				tool.Main(context.Background(), app, append([]string{"format"}, args...))
 			})
-			if isDiff {
-				got, err = stripFileHeader(got)
-				if err != nil {
-					t.Errorf("%v: got: %v\n%v", filename, err, got)
-					continue
-				}
-			}
+			got = r.normalizePaths(got)
 			// check the first two lines are the expected file header
 			if expect != got {
 				t.Errorf("format failed with %#v expected:\n%s\ngot:\n%s", args, expect, got)
@@ -81,23 +59,12 @@
 	}
 }
 
-func stripFileHeader(s string) (string, error) {
-	s = strings.TrimSpace(s)
-	if !strings.HasPrefix(s, "---") {
-		return s, fmt.Errorf("missing original")
+var unifiedHeader = regexp.MustCompile(`^diff -u.*\n(---\s+\S+\.go\.orig)\s+[\d-:. ]+(\n\+\+\+\s+\S+\.go)\s+[\d-:. ]+(\n@@)`)
+
+func fixFileHeader(s string) string {
+	match := unifiedHeader.FindStringSubmatch(s)
+	if match == nil {
+		return s
 	}
-	if i := strings.IndexRune(s, '\n'); i >= 0 && i < len(s)-1 {
-		s = s[i+1:]
-	} else {
-		return s, fmt.Errorf("no EOL for original")
-	}
-	if !strings.HasPrefix(s, "+++") {
-		return s, fmt.Errorf("missing output")
-	}
-	if i := strings.IndexRune(s, '\n'); i >= 0 && i < len(s)-1 {
-		s = s[i+1:]
-	} else {
-		return s, fmt.Errorf("no EOL for output")
-	}
-	return s, nil
+	return strings.Join(append(match[1:], s[len(match[0]):]), "")
 }
diff --git a/internal/lsp/testdata/format/bad_format.gofmt-d.golden.go b/internal/lsp/testdata/format/bad_format.gofmt-d.golden.go
index a01634c..1e101d7 100644
--- a/internal/lsp/testdata/format/bad_format.gofmt-d.golden.go
+++ b/internal/lsp/testdata/format/bad_format.gofmt-d.golden.go
@@ -1,3 +1,5 @@
+--- format/bad_format.go.orig
++++ format/bad_format.go
 @@ -1,16 +1,13 @@
  package format //@format("package")
  
@@ -14,4 +16,5 @@
 -
 -
  	var x int //@diag("x", "LSP", "x declared but not used")
- }
\ No newline at end of file
+ }
+ 
diff --git a/internal/lsp/testdata/format/newline_format.gofmt-d.golden.go b/internal/lsp/testdata/format/newline_format.gofmt-d.golden.go
index 1f356fb..4470d1e 100644
--- a/internal/lsp/testdata/format/newline_format.gofmt-d.golden.go
+++ b/internal/lsp/testdata/format/newline_format.gofmt-d.golden.go
@@ -1,5 +1,7 @@
+--- format/newline_format.go.orig
++++ format/newline_format.go
 @@ -1,2 +1,2 @@
  package format //@format("package")
 -func _() {}
 \ No newline at end of file
-+func _()       {}
\ No newline at end of file
++func _()       {}