internal/lsp: extract view to its own package

This allows us to write the lsp verbs in terms of a stable underlying source
management layer.
This should make it easier to refactor the underlying layer to add more powerful
caching and incremental modes as we go.

Change-Id: Iab97b061d80394a6fa6748a93a4c68f2deb46129
Reviewed-on: https://go-review.googlesource.com/c/147201
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index c4dbb88..94e8ff1 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -11,10 +11,11 @@
 
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/lsp/source"
 )
 
-func (v *view) diagnostics(uri protocol.DocumentURI) (map[string][]protocol.Diagnostic, error) {
-	pkg, err := v.typeCheck(uri)
+func diagnostics(v *source.View, uri protocol.DocumentURI) (map[string][]protocol.Diagnostic, error) {
+	pkg, err := v.TypeCheck(uri)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/lsp/diagnostics_test.go b/internal/lsp/diagnostics_test.go
index e547c6b..093bfc3 100644
--- a/internal/lsp/diagnostics_test.go
+++ b/internal/lsp/diagnostics_test.go
@@ -15,6 +15,7 @@
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/go/packages/packagestest"
 	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/lsp/source"
 )
 
 func TestDiagnostics(t *testing.T) {
@@ -84,12 +85,12 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	v := newView()
-	v.config = exported.Config
-	v.config.Mode = packages.LoadSyntax
+	v := source.NewView()
+	v.Config = exported.Config
+	v.Config.Mode = packages.LoadSyntax
 	for _, pkg := range pkgs {
 		for _, filename := range pkg.GoFiles {
-			diagnostics, err := v.diagnostics(filenameToURI(filename))
+			diagnostics, err := diagnostics(v, source.ToURI(filename))
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/internal/lsp/format.go b/internal/lsp/format.go
index c78cf36..9bfdb65 100644
--- a/internal/lsp/format.go
+++ b/internal/lsp/format.go
@@ -10,11 +10,12 @@
 	"go/format"
 
 	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/lsp/source"
 )
 
-// format formats a document with a given range.
-func (s *server) format(uri protocol.DocumentURI, rng *protocol.Range) ([]protocol.TextEdit, error) {
-	data, err := s.readActiveFile(uri)
+// 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)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 58cecde..63c403e 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -11,6 +11,7 @@
 
 	"golang.org/x/tools/internal/jsonrpc2"
 	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/lsp/source"
 )
 
 // RunServer starts an LSP server on the supplied stream, and waits until the
@@ -28,7 +29,7 @@
 	initializedMu sync.Mutex
 	initialized   bool // set once the server has received "initialize" request
 
-	*view
+	view *source.View
 }
 
 func (s *server) Initialize(ctx context.Context, params *protocol.InitializeParams) (*protocol.InitializeResult, error) {
@@ -37,7 +38,7 @@
 	if s.initialized {
 		return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidRequest, "server already initialized")
 	}
-	s.view = newView()
+	s.view = source.NewView()
 	s.initialized = true
 	return &protocol.InitializeResult{
 		Capabilities: protocol.ServerCapabilities{
@@ -110,15 +111,13 @@
 }
 
 func (s *server) cacheAndDiagnoseFile(ctx context.Context, uri protocol.DocumentURI, text string) {
-	s.view.activeFilesMu.Lock()
-	s.view.activeFiles[uri] = []byte(text)
-	s.view.activeFilesMu.Unlock()
+	s.view.SetActiveFileContent(uri, []byte(text))
 	go func() {
-		reports, err := s.diagnostics(uri)
+		reports, err := diagnostics(s.view, uri)
 		if err == nil {
 			for filename, diagnostics := range reports {
 				s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{
-					URI:         filenameToURI(filename),
+					URI:         source.ToURI(filename),
 					Diagnostics: diagnostics,
 				})
 			}
@@ -140,7 +139,7 @@
 }
 
 func (s *server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
-	s.clearActiveFile(params.TextDocument.URI)
+	s.view.ClearActiveFile(params.TextDocument.URI)
 	return nil
 }
 
@@ -213,11 +212,11 @@
 }
 
 func (s *server) Formatting(ctx context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) {
-	return s.format(params.TextDocument.URI, nil)
+	return formatRange(s.view, params.TextDocument.URI, nil)
 }
 
 func (s *server) RangeFormatting(ctx context.Context, params *protocol.DocumentRangeFormattingParams) ([]protocol.TextEdit, error) {
-	return s.format(params.TextDocument.URI, &params.Range)
+	return formatRange(s.view, params.TextDocument.URI, &params.Range)
 }
 
 func (s *server) OnTypeFormatting(context.Context, *protocol.DocumentOnTypeFormattingParams) ([]protocol.TextEdit, error) {
diff --git a/internal/lsp/source/uri.go b/internal/lsp/source/uri.go
new file mode 100644
index 0000000..c6b9d8f
--- /dev/null
+++ b/internal/lsp/source/uri.go
@@ -0,0 +1,32 @@
+// 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 (
+	"fmt"
+	"path/filepath"
+	"strings"
+
+	"golang.org/x/tools/internal/lsp/protocol"
+)
+
+const fileSchemePrefix = "file://"
+
+// FromURI gets the file path for a given URI.
+// It will return an error if the uri is not valid, or if the URI was not
+// a file URI
+func FromURI(uri protocol.DocumentURI) (string, error) {
+	s := string(uri)
+	if !strings.HasPrefix(s, fileSchemePrefix) {
+		return "", fmt.Errorf("only file URI's are supported, got %v", uri)
+	}
+	return filepath.FromSlash(s[len(fileSchemePrefix):]), nil
+}
+
+// ToURI returns a protocol URI for the supplied path.
+// It will always have the file scheme.
+func ToURI(path string) protocol.DocumentURI {
+	return protocol.DocumentURI(fileSchemePrefix + filepath.ToSlash(path))
+}
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
new file mode 100644
index 0000000..27fb61b
--- /dev/null
+++ b/internal/lsp/source/view.go
@@ -0,0 +1,88 @@
+// 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 (
+	"fmt"
+	"go/token"
+	"sync"
+
+	"golang.org/x/tools/go/packages"
+	"golang.org/x/tools/internal/lsp/protocol"
+)
+
+type View struct {
+	Config *packages.Config
+
+	activeFilesMu sync.Mutex
+	activeFiles   map[protocol.DocumentURI][]byte
+
+	fset *token.FileSet
+}
+
+func NewView() *View {
+	fset := token.NewFileSet()
+	return &View{
+		Config: &packages.Config{
+			Mode:  packages.LoadSyntax,
+			Fset:  fset,
+			Tests: true,
+		},
+		activeFiles: make(map[protocol.DocumentURI][]byte),
+		fset:        fset,
+	}
+}
+
+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
+		}
+	}
+	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()
+}
+
+// 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()
+	path, err := FromURI(uri)
+	if err != nil {
+		return nil, err
+	}
+	pkgs, err := packages.Load(v.Config, fmt.Sprintf("file=%s", path))
+	if len(pkgs) == 0 {
+		return nil, err
+	}
+	pkg := pkgs[0]
+	return pkg, nil
+}
diff --git a/internal/lsp/view.go b/internal/lsp/view.go
deleted file mode 100644
index 77524d6..0000000
--- a/internal/lsp/view.go
+++ /dev/null
@@ -1,87 +0,0 @@
-// 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 lsp
-
-import (
-	"fmt"
-	"go/token"
-	"strings"
-	"sync"
-
-	"golang.org/x/tools/go/packages"
-	"golang.org/x/tools/internal/lsp/protocol"
-)
-
-type view struct {
-	activeFilesMu sync.Mutex
-	activeFiles   map[protocol.DocumentURI][]byte
-	config        *packages.Config
-
-	fset *token.FileSet
-}
-
-func newView() *view {
-	fset := token.NewFileSet()
-	return &view{
-		config: &packages.Config{
-			Mode:  packages.LoadSyntax,
-			Fset:  fset,
-			Tests: true,
-		},
-		activeFiles: make(map[protocol.DocumentURI][]byte),
-		fset:        fset,
-	}
-}
-
-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 {
-		over[uriToFilename(uri)] = content
-	}
-	return over
-}
-
-func (v *view) readActiveFile(uri protocol.DocumentURI) ([]byte, error) {
-	v.activeFilesMu.Lock()
-	defer v.activeFilesMu.Unlock()
-
-	content, ok := v.activeFiles[uri]
-	if !ok {
-		return nil, fmt.Errorf("file not found: %s", uri)
-	}
-	return content, nil
-}
-
-func (v *view) clearActiveFile(uri protocol.DocumentURI) {
-	v.activeFilesMu.Lock()
-	delete(v.activeFiles, uri)
-	v.activeFilesMu.Unlock()
-}
-
-// 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()
-	pkgs, err := packages.Load(v.config, fmt.Sprintf("file=%s", uriToFilename(uri)))
-	if len(pkgs) == 0 {
-		if err == nil {
-			err = fmt.Errorf("no packages found for %s", uri)
-		}
-		return nil, err
-	}
-	pkg := pkgs[0]
-	return pkg, nil
-}
-
-func uriToFilename(uri protocol.DocumentURI) string {
-	return strings.TrimPrefix(string(uri), "file://")
-}
-
-func filenameToURI(filename string) protocol.DocumentURI {
-	return protocol.DocumentURI("file://" + filename)
-}