internal/lsp: make file a first class concept

A file is strongly associated with a view, and knows how to manage it's own
contents.
We can also now track files that are not "active"

Change-Id: Ib9474cd40e5caa3db6596548612a9f90168b8a19
Reviewed-on: https://go-review.googlesource.com/c/147204
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/format.go b/internal/lsp/format.go
index 9bfdb65..b5ce2f2 100644
--- a/internal/lsp/format.go
+++ b/internal/lsp/format.go
@@ -15,7 +15,7 @@
 
 // formatRange formats a document with a given range.
 func formatRange(v *source.View, uri protocol.DocumentURI, rng *protocol.Range) ([]protocol.TextEdit, error) {
-	data, err := v.ReadActiveFile(uri)
+	data, err := v.GetFile(uri).Read()
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 63c403e..2199ed3 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -111,7 +111,7 @@
 }
 
 func (s *server) cacheAndDiagnoseFile(ctx context.Context, uri protocol.DocumentURI, text string) {
-	s.view.SetActiveFileContent(uri, []byte(text))
+	s.view.GetFile(uri).SetContent([]byte(text))
 	go func() {
 		reports, err := diagnostics(s.view, uri)
 		if err == nil {
@@ -139,7 +139,7 @@
 }
 
 func (s *server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
-	s.view.ClearActiveFile(params.TextDocument.URI)
+	s.view.GetFile(params.TextDocument.URI).SetContent(nil)
 	return nil
 }
 
diff --git a/internal/lsp/source/file.go b/internal/lsp/source/file.go
new file mode 100644
index 0000000..2d8a4f5
--- /dev/null
+++ b/internal/lsp/source/file.go
@@ -0,0 +1,76 @@
+// Copyright 2018 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 source
+
+import (
+	"go/ast"
+	"go/token"
+	"io/ioutil"
+
+	"golang.org/x/tools/internal/lsp/protocol"
+)
+
+// File holds all the information we know about a file.
+type File struct {
+	URI     protocol.DocumentURI
+	view    *View
+	active  bool
+	content []byte
+	ast     *ast.File
+	token   *token.File
+}
+
+// SetContent sets the overlay contents for a file.
+// Setting it to nil will revert it to the on disk contents, and remove it
+// from the active set.
+func (f *File) SetContent(content []byte) {
+	f.view.mu.Lock()
+	defer f.view.mu.Unlock()
+	f.content = content
+	// the ast and token fields are invalid
+	f.ast = nil
+	f.token = nil
+	// and we might need to update the overlay
+	switch {
+	case f.active && content == nil:
+		// we were active, and want to forget the content
+		f.active = false
+		if filename, err := FromURI(f.URI); err == nil {
+			delete(f.view.Config.Overlay, filename)
+		}
+		f.content = nil
+	case content != nil:
+		// an active overlay, update the map
+		f.active = true
+		if filename, err := FromURI(f.URI); err == nil {
+			f.view.Config.Overlay[filename] = f.content
+		}
+	}
+}
+
+// Read returns the contents of the file, reading it from file system if needed.
+func (f *File) Read() ([]byte, error) {
+	f.view.mu.Lock()
+	defer f.view.mu.Unlock()
+	return f.read()
+}
+
+// read is the internal part of Read that presumes the lock is already held
+func (f *File) read() ([]byte, error) {
+	if f.content != nil {
+		return f.content, nil
+	}
+	// we don't know the content yet, so read it
+	filename, err := FromURI(f.URI)
+	if err != nil {
+		return nil, err
+	}
+	content, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return nil, err
+	}
+	f.content = content
+	return f.content, nil
+}
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 27fb61b..01332f8 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -14,67 +14,41 @@
 )
 
 type View struct {
+	mu sync.Mutex // protects all mutable state of the view
+
 	Config *packages.Config
 
-	activeFilesMu sync.Mutex
-	activeFiles   map[protocol.DocumentURI][]byte
-
-	fset *token.FileSet
+	files map[protocol.DocumentURI]*File
 }
 
 func NewView() *View {
-	fset := token.NewFileSet()
 	return &View{
 		Config: &packages.Config{
 			Mode:  packages.LoadSyntax,
-			Fset:  fset,
+			Fset:  token.NewFileSet(),
 			Tests: true,
 		},
-		activeFiles: make(map[protocol.DocumentURI][]byte),
-		fset:        fset,
+		files: make(map[protocol.DocumentURI]*File),
 	}
 }
 
-func (v *View) overlay() map[string][]byte {
-	over := make(map[string][]byte)
-
-	v.activeFilesMu.Lock()
-	defer v.activeFilesMu.Unlock()
-
-	for uri, content := range v.activeFiles {
-		filename, err := FromURI(uri)
-		if err == nil {
-			over[filename] = content
-		}
+// GetFile returns a File for the given uri.
+// It will always succeed, adding the file to the managed set if needed.
+func (v *View) GetFile(uri protocol.DocumentURI) *File {
+	v.mu.Lock()
+	f, found := v.files[uri]
+	if !found {
+		f := &File{URI: uri}
+		v.files[f.URI] = f
 	}
-	return over
-}
-
-func (v *View) SetActiveFileContent(uri protocol.DocumentURI, content []byte) {
-	v.activeFilesMu.Lock()
-	v.activeFiles[uri] = content
-	v.activeFilesMu.Unlock()
-}
-
-func (v *View) ReadActiveFile(uri protocol.DocumentURI) ([]byte, error) {
-	v.activeFilesMu.Lock()
-	content, ok := v.activeFiles[uri]
-	v.activeFilesMu.Unlock()
-	if !ok {
-		return nil, fmt.Errorf("uri not found: %s", uri)
-	}
-	return content, nil
-}
-
-func (v *View) ClearActiveFile(uri protocol.DocumentURI) {
-	v.activeFilesMu.Lock()
-	delete(v.activeFiles, uri)
-	v.activeFilesMu.Unlock()
+	v.mu.Unlock()
+	return f
 }
 
 // TypeCheck type-checks the package for the given package path.
 func (v *View) TypeCheck(uri protocol.DocumentURI) (*packages.Package, error) {
-	v.Config.Overlay = v.overlay()
+	v.mu.Lock()
+	defer v.mu.Unlock()
 	path, err := FromURI(uri)
 	if err != nil {
 		return nil, err