internal/lsp: handle first change behavior on the server side

I'm not sure why this was being managed by the view, but delete the code
that handles tracking a file's first change. It is only used to avoid
spamming the user with error messages.

Change-Id: Id95089478ffb7e189d38cbc147e3dde6a1c55c5e
Reviewed-on: https://go-review.googlesource.com/c/tools/+/208274
Reviewed-by: Ian Cottrell <iancottrell@google.com>
Reviewed-by: Heschi Kreinick <heschi@google.com>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index bbbcd9c..81f350b 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -51,9 +51,6 @@
 	// sameContentOnDisk is true if a file has been saved on disk,
 	// and therefore does not need to be part of the overlay sent to go/packages.
 	sameContentOnDisk bool
-
-	// unchanged is true if a file has not yet been edited.
-	unchanged bool
 }
 
 func (s *session) Options() source.Options {
@@ -342,7 +339,7 @@
 	return s.cache.GetFile(uri, kind)
 }
 
-func (s *session) SetOverlay(uri span.URI, kind source.FileKind, version float64, data []byte) bool {
+func (s *session) SetOverlay(uri span.URI, kind source.FileKind, version float64, data []byte) {
 	s.overlayMu.Lock()
 	defer func() {
 		s.overlayMu.Unlock()
@@ -351,22 +348,17 @@
 
 	if data == nil {
 		delete(s.overlays, uri)
-		return false
+		return
 	}
 
-	o := s.overlays[uri]
-	firstChange := o != nil && o.unchanged
-
 	s.overlays[uri] = &overlay{
-		session:   s,
-		uri:       uri,
-		kind:      kind,
-		data:      data,
-		hash:      hashContents(data),
-		version:   version,
-		unchanged: o == nil,
+		session: s,
+		uri:     uri,
+		kind:    kind,
+		data:    data,
+		hash:    hashContents(data),
+		version: version,
 	}
-	return firstChange
 }
 
 func (s *session) clearOverlay(uri span.URI) {
@@ -385,13 +377,12 @@
 		s.filesWatchMap.Notify(uri, source.Open)
 	}()
 	s.overlays[uri] = &overlay{
-		session:   s,
-		uri:       uri,
-		kind:      kind,
-		data:      data,
-		hash:      hashContents(data),
-		unchanged: true,
-		version:   version,
+		session: s,
+		uri:     uri,
+		kind:    kind,
+		data:    data,
+		hash:    hashContents(data),
+		version: version,
 	}
 	// If the file is on disk, check if its content is the same as the overlay.
 	if _, hash, err := s.cache.GetFile(uri, kind).Read(ctx); err == nil {
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index dfd239f..c8d5f91 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -324,21 +324,21 @@
 }
 
 // SetContent sets the overlay contents for a file.
-func (v *view) SetContent(ctx context.Context, uri span.URI, version float64, content []byte) (bool, error) {
+func (v *view) SetContent(ctx context.Context, uri span.URI, version float64, content []byte) {
 	v.mu.Lock()
 	defer v.mu.Unlock()
 
+	if v.Ignore(uri) {
+		return
+	}
+
 	// Cancel all still-running previous requests, since they would be
 	// operating on stale data.
 	v.cancel()
 	v.backgroundCtx, v.cancel = context.WithCancel(v.baseCtx)
 
-	if v.Ignore(uri) {
-		return false, nil
-	}
-
 	kind := source.DetectLanguage("", uri.Filename())
-	return v.session.SetOverlay(uri, kind, version, content), nil
+	v.session.SetOverlay(uri, kind, version, content)
 }
 
 // FindFile returns the file if the given URI is already a part of the view.
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index dacfd48..147061e 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -14,6 +14,7 @@
 	"golang.org/x/tools/internal/jsonrpc2"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
+	"golang.org/x/tools/internal/span"
 )
 
 // NewClientServer
@@ -83,6 +84,9 @@
 	undeliveredMu sync.Mutex
 	undelivered   map[source.FileIdentity][]source.Diagnostic
 
+	// changedFiles tracks files for which there has been a textDocument/didChange.
+	changedFiles map[span.URI]struct{}
+
 	// folders is only valid between initialize and initialized, and holds the
 	// set of folders to build views for when we are ready
 	pendingFolders []protocol.WorkspaceFolder
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 0a1ea13..1d0ab14 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -96,7 +96,7 @@
 	FindFile(ctx context.Context, uri span.URI) File
 
 	// Called to set the effective contents of a file from this view.
-	SetContent(ctx context.Context, uri span.URI, version float64, content []byte) (wasFirstChange bool, err error)
+	SetContent(ctx context.Context, uri span.URI, version float64, content []byte)
 
 	// BackgroundContext returns a context used for all background processing
 	// on behalf of this view.
@@ -172,7 +172,7 @@
 	IsOpen(uri span.URI) bool
 
 	// Called to set the effective contents of a file from this session.
-	SetOverlay(uri span.URI, kind FileKind, version float64, data []byte) (wasFirstChange bool)
+	SetOverlay(uri span.URI, kind FileKind, version float64, data []byte)
 
 	// DidChangeOutOfBand is called when a file under the root folder changes.
 	// If the file was open in the editor, it returns true.
diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go
index c821a2e..a979cca 100644
--- a/internal/lsp/text_synchronization.go
+++ b/internal/lsp/text_synchronization.go
@@ -71,14 +71,10 @@
 	if err != nil {
 		return err
 	}
-	wasFirstChange, err := view.SetContent(ctx, uri, params.TextDocument.Version, []byte(text))
-	if err != nil {
-		return err
-	}
-
-	// TODO: Ideally, we should be able to specify that a generated file should be opened as read-only.
+	view.SetContent(ctx, uri, params.TextDocument.Version, []byte(text))
+	// Ideally, we should be able to specify that a generated file should be opened as read-only.
 	// Tell the user that they should not be editing a generated file.
-	if source.IsGenerated(ctx, view, uri) && wasFirstChange {
+	if s.wasFirstChange(uri) && source.IsGenerated(ctx, view, uri) {
 		s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
 			Message: fmt.Sprintf("Do not edit this file! %s is a generated file.", uri.Filename()),
 			Type:    protocol.Warning,
@@ -91,6 +87,14 @@
 	return nil
 }
 
+func (s *Server) wasFirstChange(uri span.URI) bool {
+	if s.changedFiles == nil {
+		s.changedFiles = make(map[span.URI]struct{})
+	}
+	_, ok := s.changedFiles[uri]
+	return ok
+}
+
 func fullChange(changes []protocol.TextDocumentContentChangeEvent) (string, bool) {
 	if len(changes) > 1 {
 		return "", false
@@ -151,6 +155,6 @@
 	if err != nil {
 		return err
 	}
-	_, err = view.SetContent(ctx, uri, -1, nil)
-	return err
+	view.SetContent(ctx, uri, -1, nil)
+	return nil
 }