gopls/integration: add the replay command to replay LSP logs

Documentation is in replay/README.md

Change-Id: Ic5a2b59269d640747a9fea7bb99fb74b2d069111
Reviewed-on: https://go-review.googlesource.com/c/tools/+/209577
Run-TryBot: Peter Weinberger <pjw@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/gopls/integration/parse/parse.go b/gopls/integration/parse/parse.go
new file mode 100644
index 0000000..72e6b48
--- /dev/null
+++ b/gopls/integration/parse/parse.go
@@ -0,0 +1,231 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package parse provides functions to parse LSP logs.
+// Fully processed logs are returned by ToRLog().
+package parse
+
+import (
+	"bufio"
+	"encoding/json"
+	"fmt"
+	"io"
+	"log"
+	"os"
+	"regexp"
+	"strings"
+)
+
+// Direction is the type of message,
+type Direction int
+
+const (
+	// Clrequest from client to server has method and id
+	Clrequest Direction = iota
+	// Clresponse from server to client
+	Clresponse
+	// Svrequest from server to client, has method and id
+	Svrequest
+	// Svresponse from client to server
+	Svresponse
+	// Toserver notification has method, but no id
+	Toserver
+	// Toclient notification
+	Toclient
+	// Reporterr is an error message
+	Reporterr // errors have method and id
+)
+
+// Logmsg is the type of a parsed log entry
+type Logmsg struct {
+	Dir     Direction
+	Method  string
+	ID      string      // for requests/responses. Client and server request ids overlap
+	Elapsed string      // for responses
+	Hdr     string      // header. do we need to keep all these strings?
+	Rest    string      // the unparsed result, with newlines or not
+	Body    interface{} // the parsed result
+}
+
+// ReadLogs from a file. Most users should use TRlog().
+func ReadLogs(fname string) ([]*Logmsg, error) {
+	byid := make(map[string]int)
+	msgs := []*Logmsg{}
+	fd, err := os.Open(fname)
+	if err != nil {
+		return nil, err
+	}
+	logrdr := bufio.NewScanner(fd)
+	logrdr.Buffer(nil, 1<<25) //  a large buffer, for safety
+	logrdr.Split(logRec)
+	for i := 0; logrdr.Scan(); i++ {
+		flds := strings.SplitN(logrdr.Text(), "\n", 2)
+		if len(flds) == 1 {
+			flds = append(flds, "") // for Errors
+		}
+		msg := parselog(flds[0], flds[1])
+		if msg == nil {
+			log.Fatalf("failed to parse %q", logrdr.Text())
+			continue
+		}
+		switch msg.Dir {
+		case Clrequest, Svrequest:
+			v, err := msg.unmarshal(Requests(msg.Method))
+			if err != nil {
+				log.Fatalf("%v for %s, %T", err, msg.Method, Requests(msg.Method))
+			}
+			msg.Body = v
+		case Clresponse, Svresponse:
+			v, err := msg.doresponse()
+			if err != nil {
+				log.Fatalf("%v %s", err, msg.Method)
+			}
+			msg.Body = v
+		case Toserver, Toclient:
+			v, err := msg.unmarshal(Notifs(msg.Method))
+			if err != nil && Notifs(msg.Method) != nil {
+				log.Fatalf("%s/%T: %v", msg.Method, Notifs(msg.Method), err)
+			}
+			msg.Body = v
+		case Reporterr:
+			msg.Body = msg.ID // cause?
+		}
+		byid[msg.ID]++
+		msgs = append(msgs, msg)
+	}
+	if err = logrdr.Err(); err != nil {
+		return msgs, err
+	}
+	return msgs, nil
+}
+
+// parse a single log message, given first line, and the rest
+func parselog(first, rest string) *Logmsg {
+	if strings.HasPrefix(rest, "Params: ") {
+		rest = rest[8:]
+	} else if strings.HasPrefix(rest, "Result: ") {
+		rest = rest[8:]
+	}
+	ans := &Logmsg{Hdr: first, Rest: rest}
+	fixid := func(s string) string {
+		// emacs does (n)., gopls does (n)'.
+		s = strings.Trim(s, "()'.{)")
+		return s
+	}
+	flds := strings.Fields(first)
+	chk := func(s string, n int) bool { return strings.Contains(first, s) && len(flds) == n }
+	// gopls and emacs differ in how they report elapsed time
+	switch {
+	case chk("Sending request", 9):
+		ans.Dir = Clrequest
+		ans.Method = flds[6][1:]
+		ans.ID = fixid(flds[8][:len(flds[8])-2])
+	case chk("Received response", 11):
+		ans.Dir = Clresponse
+		ans.Method = flds[6][1:]
+		ans.ID = fixid(flds[8])
+		ans.Elapsed = flds[10]
+	case chk("Received request", 9):
+		ans.Dir = Svrequest
+		ans.Method = flds[6][1:]
+		ans.ID = fixid(flds[8])
+	case chk("Sending response", 11), // gopls
+		chk("Sending response", 13): // emacs
+		ans.Dir = Svresponse
+		ans.Method = flds[6][1:]
+		ans.ID = fixid(flds[8][:len(flds[8])-1])
+		ans.Elapsed = flds[10]
+	case chk("Sending notification", 7):
+		ans.Dir = Toserver
+		ans.Method = strings.Trim(flds[6], ".'")
+		if len(flds) == 9 {
+			log.Printf("len=%d method=%s %q", len(flds), ans.Method, first)
+		}
+	case chk("Received notification", 7):
+		ans.Dir = Toclient
+		ans.Method = flds[6][1 : len(flds[6])-2]
+	case strings.HasPrefix(first, "[Error - "):
+		//log.Printf("%s %s %v", first, rest, flds)
+		ans.Dir = Reporterr
+		both := flds[5]
+		idx := strings.Index(both, "#") // relies on ID.Number
+		ans.Method = both[:idx]
+		ans.ID = fixid(both[idx+1:])
+		ans.Rest = strings.Join(flds[6:], " ")
+	default:
+		log.Fatalf("surprise, first=%q with %d flds", first, len(flds))
+		return nil
+	}
+	return ans
+}
+
+// unmarshal into a proposed type
+func (l *Logmsg) unmarshal(p interface{}) (interface{}, error) {
+	r := []byte(l.Rest)
+	if err := json.Unmarshal(r, p); err != nil {
+		// need general alternatives, but for now
+		// if p is *[]foo and rest is {}, return an empty p (or *p?)
+		// or, cheat:
+		if l.Rest == "{}" {
+			return nil, nil
+		}
+		return nil, err
+	}
+	return p, nil
+}
+
+func (l *Logmsg) doresponse() (interface{}, error) {
+	for _, x := range Responses(l.Method) {
+		v, err := l.unmarshal(x)
+		if err == nil {
+			return v, nil
+		}
+		if x == nil {
+			return new(interface{}), nil
+		}
+	}
+	// failure!
+	rr := Responses(l.Method)
+	for _, x := range rr {
+		log.Printf("tried %T", x)
+	}
+	log.Fatalf("(%d) doresponse failed for %s %q", len(rr), l.Method, l.Rest)
+	return nil, nil
+}
+
+// be a little forgiving in separating log records
+var recSep = regexp.MustCompile("\n\n\n|\r\n\r\n\r\n")
+
+// return offset of start of next record, contents of record, error
+func logRec(b []byte, atEOF bool) (int, []byte, error) { //bufio.SplitFunc
+	got := recSep.FindIndex(b)
+	if got == nil {
+		if !atEOF {
+			return 0, nil, nil // need more
+		}
+		return 0, nil, io.EOF
+	}
+	return got[1], b[:got[0]], nil
+}
+
+// String returns a user-useful versin of a Direction
+func (d Direction) String() string {
+	switch d {
+	case Clrequest:
+		return "clrequest"
+	case Clresponse:
+		return "clresponse"
+	case Svrequest:
+		return "svrequest"
+	case Svresponse:
+		return "svresponse"
+	case Toserver:
+		return "toserver"
+	case Toclient:
+		return "toclient"
+	case Reporterr:
+		return "reporterr"
+	}
+	return fmt.Sprintf("dirname: %d unknown", d)
+}
diff --git a/gopls/integration/parse/protocol.go b/gopls/integration/parse/protocol.go
new file mode 100644
index 0000000..d812a54
--- /dev/null
+++ b/gopls/integration/parse/protocol.go
@@ -0,0 +1,310 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package parse
+
+import (
+	"log"
+
+	p "golang.org/x/tools/internal/lsp/protocol"
+)
+
+// Requests and notifications are fixed types
+// Responses may be one of several types
+
+// Requests returns a pointer to a type suitable for Unmarshal
+func Requests(m string) interface{} {
+	// these are in the documentation's order
+	switch m {
+	case "initialize":
+		return new(p.InitializeParams)
+	case "shutdown":
+		return new(struct{})
+	case "window/showMessgeRequest":
+		return new(p.ShowMessageRequestParams)
+	case "client/registerCapability":
+		return new(p.RegistrationParams)
+	case "client/unregisterCapability":
+		return new(p.UnregistrationParams)
+	case "workspace/workspaceFolders":
+		return nil
+	case "workspace/configuration":
+		return new(p.ConfigurationParams)
+	case "workspace/symbol":
+		return new(p.WorkspaceSymbolParams)
+	case "workspace/executeCommand":
+		return new(p.ExecuteCommandParams)
+	case "workspace/applyEdit":
+		return new(p.ApplyWorkspaceEditParams)
+	case "textDocument/willSaveWaitUntil":
+		return new(p.WillSaveTextDocumentParams)
+	case "textDocument/completion":
+		return new(p.CompletionParams)
+	case "completionItem/resolve":
+		return new(p.CompletionItem)
+	case "textDocument/hover":
+		return new(p.TextDocumentPositionParams)
+	case "textDocument/signatureHelp":
+		return new(p.TextDocumentPositionParams)
+	case "textDocument/declaration":
+		return new(p.TextDocumentPositionParams)
+	case "textDocument/definition":
+		return new(p.TextDocumentPositionParams)
+	case "textDocument/typeDefinition":
+		return new(p.TextDocumentPositionParams)
+	case "textDocument/implementation":
+		return new(p.TextDocumentPositionParams)
+	case "textDocument/references":
+		return new(p.ReferenceParams)
+	case "textDocument/documentHighlight":
+		return new(p.TextDocumentPositionParams)
+	case "textDocument/documentSymbol":
+		return new(p.DocumentSymbolParams)
+	case "textDocument/codeAction":
+		return new(p.CodeActionParams)
+	case "textDocument/codeLens":
+		return new(p.CodeLensParams)
+	case "codeLens/resolve":
+		return new(p.CodeLens)
+	case "textDocument/documentLink":
+		return new(p.DocumentLinkParams)
+	case "documentLink/resolve":
+		return new(p.DocumentLink)
+	case "textDocument/documentColor":
+		return new(p.DocumentColorParams)
+	case "textDocument/colorPressentation":
+		return new(p.ColorPresentationParams)
+	case "textDocument/formatting":
+		return new(p.DocumentFormattingParams)
+	case "textDocument/rangeFormatting":
+		return new(p.DocumentRangeFormattingParams)
+	case "textDocument/typeFormatting":
+		return new(p.DocumentOnTypeFormattingParams)
+	case "textDocument/rename":
+		return new(p.RenameParams)
+	case "textDocument/prepareRename":
+		return new(p.TextDocumentPositionParams)
+	case "textDocument/foldingRange":
+		return new(p.FoldingRangeParams)
+	}
+	log.Fatalf("request(%s) undefined", m)
+	return ""
+}
+
+// Notifs returns a pointer to a type suitable for Unmarshal
+func Notifs(m string) interface{} {
+	switch m {
+	case "$/cancelRequest":
+		return new(p.CancelParams)
+	case "$/setTraceNotification":
+		return new(struct{ Value string })
+	case "client/registerCapability": // why is this a notification? (serer->client rpc)
+		return new(p.RegistrationParams)
+	case "initialized":
+		return new(p.InitializedParams)
+	case "exit":
+		return nil
+	case "window/showMessage":
+		return new(p.ShowMessageParams)
+	case "window/logMessage":
+		return new(p.LogMessageParams)
+	case "telemetry/event":
+		return new(interface{}) // any
+	case "workspace/didChangeWorkspaceFolders":
+		return new(p.DidChangeWorkspaceFoldersParams)
+	case "workspace/didChangeConfiguration":
+		return new(p.DidChangeConfigurationParams)
+	case "workspace/didChangeWatchedFiles":
+		return new(p.DidChangeWatchedFilesParams)
+	case "textDocument/didOpen":
+		return new(p.DidOpenTextDocumentParams)
+	case "textDocument/didChange":
+		return new(p.DidChangeTextDocumentParams)
+	case "textDocument/willSave":
+		return new(p.WillSaveTextDocumentParams)
+	case "textDocument/didSave":
+		return new(p.DidSaveTextDocumentParams)
+	case "textDocument/didClose":
+		return new(p.DidCloseTextDocumentParams)
+	case "textDocument/willClose":
+		return new(p.DidCloseTextDocumentParams)
+	case "textDocument/publishDiagnostics":
+		return new(p.PublishDiagnosticsParams)
+	}
+	log.Fatalf("notif(%s) undefined", m)
+	return ""
+}
+
+// Responses returns a slice of types, one of which should be
+// suitable for Unmarshal
+func Responses(m string) []interface{} {
+	switch m {
+	case "initialize":
+		return []interface{}{new(p.InitializeResult)}
+	case "shutdown":
+		return []interface{}{nil}
+	case "window/showMessageRequest":
+		return []interface{}{new(p.MessageActionItem), nil}
+	case "client/registerCapability":
+		return []interface{}{nil}
+	case "client/unregisterCapability":
+		return []interface{}{nil}
+	case "workspace/workspaceFolder":
+		return []interface{}{new([]p.WorkspaceFolder), nil}
+	case "workspace/configuration":
+		return []interface{}{new([]interface{}), new(interface{})}
+	case "workspace/symbol":
+		return []interface{}{new([]p.SymbolInformation), nil}
+	case "workspace/executeCommand":
+		return []interface{}{new(interface{}), nil}
+	case "workspace/applyEdit":
+		return []interface{}{new(p.ApplyWorkspaceEditResponse)}
+	case "textDocument/willSaveWaitUntil":
+		return []interface{}{new([]p.TextEdit), nil}
+	case "textDocument/completion":
+		return []interface{}{new(p.CompletionList), new([]p.CompletionItem), nil}
+	case "completionItem/resolve":
+		return []interface{}{new(p.CompletionItem)}
+	case "textDocument/hover":
+		return []interface{}{new(p.Hover), nil}
+	case "textDocument/signatureHelp":
+		return []interface{}{new(p.SignatureHelp), nil}
+	case "textDocument/declaration":
+		return []interface{}{new(p.Location), new([]p.Location), new([]p.LocationLink), nil}
+	case "textDocument/definition":
+		return []interface{}{new([]p.Location), new([]p.Location), new([]p.LocationLink), nil}
+	case "textDocument/typeDefinition":
+		return []interface{}{new([]p.Location), new([]p.LocationLink), new(p.Location), nil}
+	case "textDocument/implementation":
+		return []interface{}{new(p.Location), new([]p.Location), new([]p.LocationLink), nil}
+	case "textDocument/references":
+		return []interface{}{new([]p.Location), nil}
+	case "textDocument/documentHighlight":
+		return []interface{}{new([]p.DocumentHighlight), nil}
+	case "textDocument/documentSymbol":
+		return []interface{}{new([]p.DocumentSymbol), new([]p.SymbolInformation), nil}
+	case "textDocument/codeAction":
+		return []interface{}{new([]p.CodeAction), new(p.Command), nil}
+	case "textDocument/codeLens":
+		return []interface{}{new([]p.CodeLens), nil}
+	case "codelens/resolve":
+		return []interface{}{new(p.CodeLens)}
+	case "textDocument/documentLink":
+		return []interface{}{new([]p.DocumentLink), nil}
+	case "documentLink/resolve":
+		return []interface{}{new(p.DocumentLink)}
+	case "textDocument/documentColor":
+		return []interface{}{new([]p.ColorInformation)}
+	case "textDocument/colorPresentation":
+		return []interface{}{new([]p.ColorPresentation)}
+	case "textDocument/formatting":
+		return []interface{}{new([]p.TextEdit), nil}
+	case "textDocument/rangeFormatting":
+		return []interface{}{new([]p.TextEdit), nil}
+	case "textDocument/onTypeFormatting":
+		return []interface{}{new([]p.TextEdit), nil}
+	case "textDocument/rename":
+		return []interface{}{new(p.WorkspaceEdit), nil}
+	case "textDocument/prepareRename":
+		return []interface{}{new(p.Range), nil}
+	case "textDocument/foldingRange":
+		return []interface{}{new([]p.FoldingRange), nil}
+	}
+	log.Fatalf("responses(%q) undefined", m)
+	return nil
+}
+
+// Msgtype given method names. Note that mSrv|mCl is possible
+type Msgtype int
+
+const (
+	// Mnot for notifications
+	Mnot Msgtype = 1
+	// Mreq for requests
+	Mreq Msgtype = 2
+	// Msrv for messages from the server
+	Msrv Msgtype = 4
+	// Mcl for messages from the client
+	Mcl Msgtype = 8
+)
+
+// IsNotify says if the message is a notification
+func IsNotify(msg string) bool {
+	m, ok := fromMethod[msg]
+	if !ok {
+		log.Fatalf("%q", msg)
+	}
+	return m&Mnot != 0
+}
+
+// FromServer says if the message is from the server
+func FromServer(msg string) bool {
+	m, ok := fromMethod[msg]
+	if !ok {
+		log.Fatalf("%q", msg)
+	}
+	return m&Msrv != 0
+}
+
+// FromClient says if the message is from the client
+func FromClient(msg string) bool {
+	m, ok := fromMethod[msg]
+	if !ok {
+		log.Fatalf("%q", msg)
+	}
+	return m&Mcl != 0
+}
+
+// rpc name to message type
+var fromMethod = map[string]Msgtype{
+	"$/cancelRequest":             Mnot | Msrv | Mcl,
+	"initialize":                  Mreq | Msrv,
+	"initialized":                 Mnot | Mcl,
+	"shutdown":                    Mreq | Mcl,
+	"exit":                        Mnot | Mcl,
+	"window/showMessage":          Mreq | Msrv,
+	"window/logMessage":           Mnot | Msrv,
+	"telemetry'event":             Mnot | Msrv,
+	"client/registerCapability":   Mreq | Msrv,
+	"client/unregisterCapability": Mreq | Msrv,
+	"workspace/workspaceFolders":  Mreq | Msrv,
+	"workspace/workspaceDidChangeWorkspaceFolders": Mnot | Mcl,
+	"workspace/didChangeConfiguration":             Mnot | Mcl,
+	"workspace/configuration":                      Mreq | Msrv,
+	"workspace/didChangeWatchedFiles":              Mnot | Mcl,
+	"workspace/symbol":                             Mreq | Mcl,
+	"workspace/executeCommand":                     Mreq | Mcl,
+	"workspace/applyEdit":                          Mreq | Msrv,
+	"textDocument/didOpen":                         Mnot | Mcl,
+	"textDocument/didChange":                       Mnot | Mcl,
+	"textDocument/willSave":                        Mnot | Mcl,
+	"textDocument/willSaveWaitUntil":               Mreq | Mcl,
+	"textDocument/didSave":                         Mnot | Mcl,
+	"textDocument/didClose":                        Mnot | Mcl,
+	"textDocument/publishDiagnostics":              Mnot | Msrv,
+	"textDocument/completion":                      Mreq | Mcl,
+	"completionItem/resolve":                       Mreq | Mcl,
+	"textDocument/hover":                           Mreq | Mcl,
+	"textDocument/signatureHelp":                   Mreq | Mcl,
+	"textDocument/declaration":                     Mreq | Mcl,
+	"textDocument/definition":                      Mreq | Mcl,
+	"textDocument/typeDefinition":                  Mreq | Mcl,
+	"textDocument/implementation":                  Mreq | Mcl,
+	"textDocument/references":                      Mreq | Mcl,
+	"textDocument/documentHighlight":               Mreq | Mcl,
+	"textDocument/documentSymbol":                  Mreq | Mcl,
+	"textDocument/codeAction":                      Mreq | Mcl,
+	"textDocument/codeLens":                        Mreq | Mcl,
+	"codeLens/resolve":                             Mreq | Mcl,
+	"textDocument/documentLink":                    Mreq | Mcl,
+	"documentLink/resolve":                         Mreq | Mcl,
+	"textDocument/documentColor":                   Mreq | Mcl,
+	"textDocument/colorPresentation":               Mreq | Mcl,
+	"textDocument/formatting":                      Mreq | Mcl,
+	"textDocument/rangeFormatting":                 Mreq | Mcl,
+	"textDocument/onTypeFormatting":                Mreq | Mcl,
+	"textDocument/rename":                          Mreq | Mcl,
+	"textDocument/prepareRename":                   Mreq | Mcl,
+	"textDocument/foldingRange":                    Mreq | Mcl,
+}
diff --git a/gopls/integration/parse/rlog.go b/gopls/integration/parse/rlog.go
new file mode 100644
index 0000000..00cd16c
--- /dev/null
+++ b/gopls/integration/parse/rlog.go
@@ -0,0 +1,125 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package parse
+
+import (
+	"fmt"
+	"log"
+	"strings"
+)
+
+// Rlog contains the processed logs
+type Rlog struct {
+	Logs         []*Logmsg          // In the order in the log file
+	ServerCall   map[string]*Logmsg // ID->Request, client->server
+	ServerReply  map[string]*Logmsg // ID->Response, server->client (includes Errors)
+	ClientCall   map[string]*Logmsg
+	ClientReply  map[string]*Logmsg
+	ClientNotifs []*Logmsg
+	ServerNotifs []*Logmsg
+	Histogram    *LogHist
+}
+
+func newRlog(x []*Logmsg) *Rlog {
+	return &Rlog{Logs: x,
+		ServerCall:   make(map[string]*Logmsg),
+		ServerReply:  make(map[string]*Logmsg),
+		ClientCall:   make(map[string]*Logmsg),
+		ClientReply:  make(map[string]*Logmsg),
+		ClientNotifs: []*Logmsg{},
+		ServerNotifs: []*Logmsg{},
+		Histogram:    &LogHist{},
+	}
+}
+
+// Counts returns a one-line summary of an Rlog
+func (r *Rlog) Counts() string {
+	return fmt.Sprintf("logs:%d srvC:%d srvR:%d clC:%d clR:%d clN:%d srvN:%d",
+		len(r.Logs),
+		len(r.ServerCall), len(r.ServerReply), len(r.ClientCall), len(r.ClientReply),
+		len(r.ClientNotifs), len(r.ServerNotifs))
+}
+
+// ToRlog reads a log file and returns a *Rlog
+func ToRlog(fname string) (*Rlog, error) {
+	x, err := ReadLogs(fname)
+	if err != nil {
+		return nil, err
+	}
+	ans := newRlog(x)
+	for _, l := range x {
+		switch l.Dir {
+		case Clrequest:
+			ans.ServerCall[l.ID] = l
+		case Clresponse:
+			ans.ServerReply[l.ID] = l
+			if l.Dir != Reporterr {
+				n := 0
+				fmt.Sscanf(l.Elapsed, "%d", &n)
+				ans.Histogram.add(n)
+			}
+		case Svrequest:
+			ans.ClientCall[l.ID] = l
+		case Svresponse:
+			ans.ClientReply[l.ID] = l
+		case Toclient:
+			ans.ClientNotifs = append(ans.ClientNotifs, l)
+		case Toserver:
+			ans.ServerNotifs = append(ans.ServerNotifs, l)
+		case Reporterr:
+			ans.ServerReply[l.ID] = l
+		default:
+			log.Fatalf("eh? %s/%s (%s)", l.Dir, l.Method, l.ID)
+		}
+	}
+	return ans, nil
+}
+
+// LogHist gets ints, and puts them into buckets:
+// <=10, <=30, 100, 300, 1000, ...
+// It produces a historgram of elapsed times in milliseconds
+type LogHist struct {
+	cnts []int
+}
+
+func (l *LogHist) add(n int) {
+	if n < 0 {
+		n = 0
+	}
+	bucket := 0
+	for ; n > 0; n /= 10 {
+		if n < 10 {
+			break
+		}
+		if n < 30 {
+			bucket++
+			break
+		}
+		bucket += 2
+	}
+	if len(l.cnts) <= bucket {
+		for j := len(l.cnts); j < bucket+10; j++ {
+			l.cnts = append(l.cnts, 0)
+		}
+	}
+	l.cnts[bucket]++
+}
+
+// String returns a string describing a histogram
+func (l *LogHist) String() string {
+	top := len(l.cnts) - 1
+	for ; top > 0 && l.cnts[top] == 0; top-- {
+	}
+	labs := []string{"10", "30"}
+	out := strings.Builder{}
+	out.WriteByte('[')
+	for i := 0; i <= top; i++ {
+		label := labs[i%2]
+		labs[i%2] += "0"
+		fmt.Fprintf(&out, "%s:%d ", label, l.cnts[i])
+	}
+	out.WriteByte(']')
+	return out.String()
+}
diff --git a/gopls/integration/replay/README.md b/gopls/integration/replay/README.md
new file mode 100644
index 0000000..3b29538
--- /dev/null
+++ b/gopls/integration/replay/README.md
@@ -0,0 +1,79 @@
+# Replaying Logs
+
+The LSP log replayer takes a log from a gopls session, starts up an instance of goppls,
+and tries to replay the session. It produces a log from the replayed session and reports
+some comparative statistics of the two logs.
+
+```replay -log <logfile>```
+
+The `logfile` should be the log produced by gopls. It will have a name like
+`/tmp/gopls-89775` or, on a Mac, `$TMPDIR/gopls-29388`.
+
+If `replay` cannot find a copy of gopls to execute, use `-cmd <path to gopls>`.
+It looks in the same places where `go install` would put its output,
+namely `$GOBIN/gopls`, `$GOPATH/bin/gopls`, `$HOME/go/bin/gopls`.
+
+The log for the replayed session is saved in `/tmp/seen`.
+
+There is aloso a boolean argument `-cmp` which compares the log file
+with `/tmp/seen` without invoking gopls and rerunning the session.
+
+The output is fairly cryptic, and generated by logging. Ideas for better output would be welcome.
+Here's an example, with intermingled comments:
+
+```
+main.go:50: old 1856, hist:[10:177 30:1 100:0 300:3 1000:4 ]
+```
+This says that the original log had 1856 records in it. The histogram is
+counting how long RPCs took, in milliseconds. In this case 177 took no more
+than 10ms, and 4 took between 300ms and 1000ms.
+```
+main.go:53: calling mimic
+main.go:293: mimic 1856
+```
+This is a reminder that it's replaying in a new session, with a log file
+containing 1856 records
+```
+main.go:61: new 1846, hist:[10:181 30:1 100:1 300:1 1000:1 ]
+```
+The new session produced 1846 log records (that's 10 fewer),
+and a vaguely similar histogram.
+```
+main.go:96: old: clrequest:578 clresponse:185 svrequest:2 svresponse:2 toserver:244 toclient:460 reporterr:385
+main.go:96: new: clrequest:571 clresponse:185 svrequest:2 svresponse:2 toserver:241 toclient:460 reporterr:385
+```
+The first line is for the original log, the second for the new log. The new log has 7 fewer RPC requests
+from the client *clrequest* (578 vs 571), the same number of client responses *clresponse*, 3 fewer
+notifications *toserver* from the client, the same number from the server *toclient* to the client, and
+the same number of errors *reporterr*. (That's mysterious, but a look at the ends of the log files shows
+that the original session ended with several RPCs that don't show up, for whatever reason, in the new session.)
+
+Finally, there are counts of the various nofications seen, in the new log and the old log, and
+which direction they went. (The 3 fewer notifications in the summary above can be seen here to be from cancels
+and a didChange.)
+```
+main.go:107: counts of notifications
+main.go:110:  '$/cancelRequest'. new toserver 1
+main.go:110:  '$/cancelRequest'. old toserver 3
+main.go:110:  'initialized'. new toserver 1
+main.go:110:  'initialized'. old toserver 1
+main.go:110:  'textDocument/didChange'. new toserver 231
+main.go:110:  'textDocument/didChange'. old toserver 232
+main.go:110:  'textDocument/didOpen'. new toserver 1
+main.go:110:  'textDocument/didOpen'. old toserver 1
+main.go:110:  'textDocument/didSave'. new toserver 7
+main.go:110:  'textDocument/didSave'. old toserver 7
+main.go:110:  'textDocument/publishDiagnostics'. new toclient 182
+main.go:110:  'textDocument/publishDiagnostics'. old toclient 182
+main.go:110:  'window/logMessage'. new toclient 278
+main.go:110:  'window/logMessage'. old toclient 278
+```
+### Caveats
+Replay cannot restore the exact environment gopls saw for the original session.
+For instance, the first didOpen message in the new session will see the file
+as it was left by the original session.
+
+Gopls invokes various tools, and the environment they see could have changed too.
+
+Replay will use the gopls it finds (or is given). It has no way of using
+the same version that created the original session.
\ No newline at end of file
diff --git a/gopls/integration/replay/main.go b/gopls/integration/replay/main.go
new file mode 100644
index 0000000..92993cf
--- /dev/null
+++ b/gopls/integration/replay/main.go
@@ -0,0 +1,575 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Replay logs. See README.md
+package main
+
+import (
+	"bufio"
+	"context"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"io"
+	"log"
+	"os"
+	"os/exec"
+	"regexp"
+	"runtime"
+	"sort"
+	"strconv"
+	"strings"
+
+	"golang.org/x/tools/gopls/integration/parse"
+	"golang.org/x/tools/internal/jsonrpc2"
+	p "golang.org/x/tools/internal/lsp/protocol"
+)
+
+var (
+	ctx     = context.Background()
+	command = flag.String("cmd", "", "location of server to send to, looks for gopls")
+	logf    = flag.String("log", "", "log file to replay")
+	cmp     = flag.Bool("cmp", false, "only compare log and /tmp/seen")
+	logrdr  *bufio.Scanner
+	msgs    []*parse.Logmsg
+	// requests and responses/errors, by id
+	clreq  = make(map[string]*logmsg)
+	clresp = make(map[string]*logmsg)
+	svreq  = make(map[string]*logmsg)
+	svresp = make(map[string]*logmsg)
+)
+
+func main() {
+	log.SetFlags(log.Lshortfile)
+	flag.Parse()
+	if *logf == "" {
+		log.Fatal("need -log")
+	}
+
+	orig, err := parse.ToRlog(*logf)
+	if err != nil {
+		log.Fatalf("logfile %q %v", *logf, err)
+	}
+	msgs = orig.Logs
+	log.Printf("old %d, hist:%s", len(msgs), orig.Histogram)
+
+	if !*cmp {
+		log.Print("calling mimic")
+		mimic()
+	}
+	seen, err := parse.ToRlog("/tmp/seen")
+	if err != nil {
+		log.Fatal(err)
+	}
+	vvv := seen.Logs
+	log.Printf("new %d, hist:%s", len(vvv), seen.Histogram)
+
+	ok := make(map[string]int)
+	f := func(x []*parse.Logmsg, label string, diags map[string][]p.Diagnostic) {
+		cnts := make(map[parse.Direction]int)
+		for _, l := range x {
+			if l.Method == "window/logMessage" {
+				// don't care
+				//continue
+			}
+			if l.Method == "textDocument/publishDiagnostics" {
+				v, ok := l.Body.(*p.PublishDiagnosticsParams)
+				if !ok {
+					log.Fatalf("got %T expected PublishDiagnosticsParams", l.Body)
+				}
+				diags[v.URI] = v.Diagnostics
+			}
+			cnts[l.Dir]++
+			// notifications only
+			if l.Dir != parse.Toserver && l.Dir != parse.Toclient {
+				continue
+			}
+			s := fmt.Sprintf("%s %s %s", strings.Replace(l.Hdr, "\r", "", -1), label, l.Dir)
+			if i := strings.Index(s, "notification"); i != -1 {
+				s = s[i+12:]
+			}
+			if len(s) > 120 {
+				s = s[:120]
+			}
+			ok[s]++
+		}
+		msg := ""
+		for i := parse.Clrequest; i <= parse.Reporterr; i++ {
+			msg += fmt.Sprintf("%s:%d ", i, cnts[i])
+		}
+		log.Printf("%s: %s", label, msg)
+	}
+	mdiags := make(map[string][]p.Diagnostic)
+	f(msgs, "old", mdiags)
+	vdiags := make(map[string][]p.Diagnostic)
+	f(vvv, "new", vdiags)
+	buf := []string{}
+	for k := range ok {
+		buf = append(buf, fmt.Sprintf("%s %d", k, ok[k]))
+	}
+	if len(buf) > 0 {
+		log.Printf("counts of notifications")
+		sort.Strings(buf)
+		for _, k := range buf {
+			log.Print(k)
+		}
+	}
+	buf = buf[0:0]
+	for k, v := range mdiags {
+		va := vdiags[k]
+		if len(v) != len(va) {
+			buf = append(buf, fmt.Sprintf("new has %d, old has %d for %s",
+				len(va), len(v), k))
+		}
+	}
+	for ka := range vdiags {
+		if _, ok := mdiags[ka]; !ok {
+			buf = append(buf, fmt.Sprintf("new diagnostics, but no old ones, for %s",
+				ka))
+		}
+	}
+	if len(buf) > 0 {
+		log.Print("diagnostics differ:")
+		for _, s := range buf {
+			log.Print(s)
+		}
+	}
+}
+
+type direction int // what sort of message it is
+const (
+	// rpc from client to server have method and id
+	clrequest direction = iota
+	clresponse
+	// rpc from server have method and id
+	svrequest
+	svresponse
+	// notifications have method, but no id
+	toserver
+	toclient
+	reporterr // errors have method and id
+)
+
+// clrequest has method and id. toserver has method but no id, and svresponse has result (and id)
+type logmsg struct {
+	dir     direction
+	method  string
+	id      string      // for requests/responses. Client and server request ids overlap
+	elapsed string      // for responses
+	hdr     string      // do we need to keep all these strings?
+	rest    string      // the unparsed result, with newlines or not
+	body    interface{} // the parsed(?) result
+}
+
+// combined has all the fields of both Request and Response.
+// Unmarshal this and then work out which it is.
+type combined struct {
+	VersionTag jsonrpc2.VersionTag `json:"jsonrpc"`
+	ID         *jsonrpc2.ID        `json:"id,omitempty"`
+	// RPC name
+	Method string           `json:"method"`
+	Params *json.RawMessage `json:"params,omitempty"`
+	Result *json.RawMessage `json:"result,omitempty"`
+	Error  *jsonrpc2.Error  `json:"error,omitempty"`
+}
+
+func (c *combined) dir() direction {
+	// Method, Params, ID => request
+	// Method, Params, no-ID => notification
+	// Error => error response
+	// Result, ID => response
+	if c.Error != nil {
+		return reporterr
+	}
+	if c.Params != nil && c.ID != nil {
+		// $/cancel could be either, cope someday
+		if parse.FromServer(c.Method) {
+			return svrequest
+		}
+		return clrequest
+	}
+	if c.Params != nil {
+		// we're receiving it, so it must be toclient
+		return toclient
+	}
+	if c.Result == nil {
+		if c.ID != nil {
+			return clresponse
+		}
+		log.Printf("%+v", *c)
+		panic("couldn't determine direction")
+	}
+	// we've received it, so it must be clresponse
+	return clresponse
+}
+
+func send(l *parse.Logmsg, stream jsonrpc2.Stream, id *jsonrpc2.ID) {
+	x, err := json.Marshal(l.Body)
+	if err != nil {
+		log.Fatal(err)
+	}
+	y := json.RawMessage(x)
+	if id == nil {
+		// need to use the number version of ID
+		n, err := strconv.Atoi(l.ID)
+		if err != nil {
+			n = 0
+		}
+		id = &jsonrpc2.ID{Number: int64(n)}
+	}
+	var r interface{}
+	switch l.Dir {
+	case parse.Clrequest:
+		r = jsonrpc2.WireRequest{
+			ID:     id,
+			Method: l.Method,
+			Params: &y,
+		}
+	case parse.Svresponse:
+		r = jsonrpc2.WireResponse{
+			ID:     id,
+			Result: &y,
+		}
+	case parse.Toserver:
+		r = jsonrpc2.WireRequest{
+			Method: l.Method,
+			Params: &y,
+		}
+	default:
+		log.Fatalf("sending %s", l.Dir)
+	}
+	data, err := json.Marshal(r)
+	if err != nil {
+		log.Fatal(err)
+	}
+	stream.Write(ctx, data)
+}
+
+func strID(x *jsonrpc2.ID) string {
+	if x.Name != "" {
+		log.Printf("strID returns %s", x.Name)
+		return x.Name
+	}
+	return strconv.Itoa(int(x.Number))
+}
+
+func respond(c *combined, stream jsonrpc2.Stream) {
+	// c is a server request
+	// pick out the id, and look for the response in msgs
+	id := strID(c.ID)
+	for _, l := range msgs {
+		if l.ID == id && l.Dir == parse.Svresponse {
+			// check that the methods match?
+			// need to send back the same ID we got.
+			send(l, stream, c.ID)
+			return
+		}
+	}
+	log.Fatalf("no response found %q %+v %+v", c.Method, c.ID, c)
+}
+
+func findgopls() string {
+	totry := [][]string{{"GOBIN", "/gopls"}, {"GOPATH", "/bin/gopls"}, {"HOME", "/go/bin/gopls"}}
+	// looks in the places go install would install:
+	// GOBIN, else GOPATH/bin, else HOME/go/bin
+	ok := func(s string) bool {
+		fd, err := os.Open(s)
+		if err != nil {
+			return false
+		}
+		fi, err := fd.Stat()
+		if err != nil {
+			return false
+		}
+		return fi.Mode()&0111 != 0
+	}
+	for _, t := range totry {
+		g := os.Getenv(t[0])
+		if g != "" && ok(g+t[1]) {
+			return g + t[1]
+		}
+	}
+	log.Fatal("could not find gopls")
+	return ""
+}
+
+func mimic() {
+	log.Printf("mimic %d", len(msgs))
+	if *command == "" {
+		*command = findgopls()
+	}
+	cmd := exec.Command(*command, "-logfile", "/tmp/seen", "-rpc.trace")
+	toServer, err := cmd.StdinPipe()
+	if err != nil {
+		log.Fatal(err)
+	}
+	fromServer, err := cmd.StdoutPipe()
+	if err != nil {
+		log.Fatal(err)
+	}
+	err = cmd.Start()
+	if err != nil {
+		log.Fatal(err)
+	}
+	stream := jsonrpc2.NewHeaderStream(fromServer, toServer)
+	rchan := make(chan *combined, 10) // do we need buffering?
+	rdr := func() {
+		for {
+			buf, _, err := stream.Read(ctx)
+			if err != nil {
+				rchan <- nil // close it instead?
+				return
+			}
+			msg := &combined{}
+			if err := json.Unmarshal(buf, msg); err != nil {
+				log.Fatal(err)
+			}
+			rchan <- msg
+		}
+	}
+	go rdr()
+	// send as many as possible: all clrequests and toservers up to a clresponse
+	// and loop
+	seenids := make(map[string]bool) // id's that have been responded toig:
+big:
+	for _, l := range msgs {
+		switch l.Dir {
+		case parse.Toserver: // just send these as we get to them
+			send(l, stream, nil)
+		case parse.Clrequest:
+			send(l, stream, nil) // for now, wait for a response, to make sure code is ok
+			fallthrough
+		case parse.Clresponse, parse.Reporterr: // don't go past these until they're received
+			if seenids[l.ID] {
+				break // onward, as it has been received already
+			}
+		done:
+			for {
+				x := <-rchan
+				if x == nil {
+					break big
+				}
+				// if it's svrequest, do something
+				// if it's clresponse or reporterr, add to seenids, and if it
+				// is l.id, break out of the loop, and continue the outer loop
+				switch x.dir() {
+				case svrequest:
+					respond(x, stream)
+					continue done // still waiting
+				case clresponse, reporterr:
+					id := strID(x.ID)
+					seenids[id] = true
+					if id == l.ID {
+						break done
+					}
+				case toclient:
+					continue
+				default:
+					log.Fatalf("%s", x.dir())
+				}
+			}
+		case parse.Svrequest: // not ours to send
+			continue
+		case parse.Svresponse: // sent by us, if the request arrives
+			continue
+		case parse.Toclient: // we don't send these
+			continue
+		}
+	}
+}
+
+func readLogs(fname string) []*logmsg {
+	byid := make(map[string]int)
+	msgs := []*logmsg{}
+	fd, err := os.Open(fname)
+	if err != nil {
+		log.Fatal(err)
+	}
+	logrdr = bufio.NewScanner(fd)
+	logrdr.Buffer(nil, 1<<25) //  a large buffer, for safety
+	logrdr.Split(logRec)
+	for i := 0; logrdr.Scan(); i++ {
+		flds := strings.SplitN(logrdr.Text(), "\n", 2)
+		if len(flds) == 1 {
+			flds = append(flds, "") // for Errors
+		}
+		msg := parselog(flds[0], flds[1])
+		if msg == nil {
+			log.Fatalf("failed to parse %q", logrdr.Text())
+			continue
+		}
+		switch msg.dir {
+		case clrequest, svrequest:
+			v, err := msg.unmarshal(parse.Requests(msg.method))
+			if err != nil {
+				log.Fatal(err)
+			}
+			msg.body = v
+		case clresponse, svresponse:
+			v, err := msg.doresponse()
+			if err != nil {
+				log.Fatalf("%v %s", err, msg.method)
+			}
+			msg.body = v
+		case toserver, toclient:
+			v, err := msg.unmarshal(parse.Notifs(msg.method))
+			if err != nil {
+				log.Fatal(err)
+			}
+			msg.body = v
+		case reporterr:
+			msg.body = msg.id // cause?
+		}
+		byid[msg.id]++
+		msgs = append(msgs, msg)
+	}
+	if err = logrdr.Err(); err != nil {
+		log.Fatal(err)
+		return msgs
+	}
+	// there's 2 uses of id 1, and notifications have no id
+	for k, v := range byid {
+		if false && v != 2 {
+			log.Printf("ids %s:%d", k, v)
+		}
+	}
+	if false {
+		var m runtime.MemStats
+		runtime.ReadMemStats(&m)
+		log.Printf("%d msgs, alloc=%d HeapAlloc=%d", len(msgs), m.Alloc, m.HeapAlloc)
+	}
+	return msgs
+}
+
+func (d direction) String() string {
+	switch d {
+	case clrequest:
+		return "clrequest"
+	case clresponse:
+		return "clresponse"
+	case svrequest:
+		return "svrequest"
+	case svresponse:
+		return "svresponse"
+	case toserver:
+		return "toserver"
+	case toclient:
+		return "toclient"
+	case reporterr:
+		return "reporterr"
+	}
+	return fmt.Sprintf("dirname: %d unknown", d)
+}
+
+func (l *logmsg) Short() string {
+	return fmt.Sprintf("%s %s %s %s", l.dir, l.method, l.id, l.elapsed)
+}
+
+func (l *logmsg) unmarshal(p interface{}) (interface{}, error) {
+	r := []byte(l.rest)
+	if err := json.Unmarshal(r, p); err != nil {
+		// need general alternatives, but for now
+		// if p is *[]foo and rest is {}, return an empty p (or *p?)
+		// or, cheat:
+		if l.rest == "{}" {
+			return nil, nil
+		}
+		//log.Fatalf("%s/%s %T %q %v", l.method, l.id, p, l.rest, err)
+		return nil, err
+	}
+	return p, nil
+}
+
+func (l *logmsg) doresponse() (interface{}, error) {
+	for _, x := range parse.Responses(l.method) {
+		v, err := l.unmarshal(x)
+		if err == nil {
+			return v, nil
+		}
+		if x == nil {
+			return new(interface{}), nil
+		}
+	}
+	log.Fatalf("doresponse failed for %s", l.method)
+	return nil, nil
+}
+
+// parse a single log message, given first line, and the rest
+func parselog(first, rest string) *logmsg {
+	if strings.HasPrefix(rest, "Params: ") {
+		rest = rest[8:]
+	} else if strings.HasPrefix(rest, "Result: ") {
+		rest = rest[8:]
+	}
+	ans := &logmsg{hdr: first, rest: rest}
+	fixid := func(s string) string {
+		if s != "" && s[0] == '(' {
+			s = s[1 : len(s)-1]
+		}
+		return s
+	}
+	flds := strings.Fields(first)
+	chk := func(s string, n int) bool { return strings.Contains(first, s) && len(flds) == n }
+	// gopls and emacs differ in how they report elapsed time
+	switch {
+	case chk("Sending request", 9):
+		ans.dir = clrequest
+		ans.method = flds[6][1:]
+		ans.id = fixid(flds[8][:len(flds[8])-2])
+		clreq[ans.id] = ans
+	case chk("Received response", 11):
+		ans.dir = clresponse
+		ans.method = flds[6][1:]
+		ans.id = fixid(flds[8][:len(flds[8])-1])
+		ans.elapsed = flds[10]
+		clresp[ans.id] = ans
+	case chk("Received request", 9):
+		ans.dir = svrequest
+		ans.method = flds[6][1:]
+		ans.id = fixid(flds[8][:len(flds[8])-2])
+		svreq[ans.id] = ans
+	case chk("Sending response", 11), // gopls
+		chk("Sending response", 13): // emacs
+		ans.dir = svresponse
+		ans.method = flds[6][1:]
+		ans.id = fixid(flds[8][:len(flds[8])-1])
+		ans.elapsed = flds[10]
+		svresp[ans.id] = ans
+	case chk("Sending notification", 7):
+		ans.dir = toserver
+		ans.method = strings.Trim(flds[6], ".'")
+		if len(flds) == 9 {
+			log.Printf("len=%d method=%s %q", len(flds), ans.method, first)
+		}
+	case chk("Received notification", 7):
+		ans.dir = toclient
+		ans.method = flds[6][1 : len(flds[6])-2]
+	case strings.HasPrefix(first, "[Error - "):
+		ans.dir = reporterr
+		both := flds[5]
+		idx := strings.Index(both, "#") // relies on ID.Number
+		ans.method = both[:idx]
+		ans.id = fixid(both[idx+1:])
+		ans.rest = strings.Join(flds[6:], " ")
+		clreq[ans.id] = ans
+	default:
+		log.Fatalf("surprise, first=%q with %d flds", first, len(flds))
+		return nil
+	}
+	return ans
+}
+
+var recSep = regexp.MustCompile("\n\n\n|\r\n\r\n\r\n")
+
+// return start of next record, contents of record, error
+func logRec(b []byte, atEOF bool) (int, []byte, error) { //bufio.SplitFunc
+	got := recSep.FindIndex(b)
+	if got == nil {
+		if !atEOF {
+			return 0, nil, nil // need more
+		}
+		return 0, nil, io.EOF
+	}
+	return got[1], b[:got[0]], nil
+}