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