internal/lsp: refactor server.go to separate into LSP categories

This change separates the different behaviors of server.go by the
categories defined in the spec. This allows us to differentiate more
easily between the language features and the text synchronization code.

I also renamed the "Symbols" function to "Symbol", which fits with the
specification
(https://microsoft.github.io/language-server-protocol/specification#workspace_symbol),
and makes clearer the distinction between DocumentSymbols and Symbol.

Change-Id: I926b8a772c478f6ae426352fb12dc4403f0e736a
Reviewed-on: https://go-review.googlesource.com/c/tools/+/172637
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/internal/lsp/definition.go b/internal/lsp/definition.go
new file mode 100644
index 0000000..5a1fe94
--- /dev/null
+++ b/internal/lsp/definition.go
@@ -0,0 +1,81 @@
+// 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 lsp
+
+import (
+	"context"
+
+	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/lsp/source"
+	"golang.org/x/tools/internal/span"
+)
+
+func (s *Server) definition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) {
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	f, m, err := newColumnMap(ctx, view, uri)
+	if err != nil {
+		return nil, err
+	}
+	spn, err := m.PointSpan(params.Position)
+	if err != nil {
+		return nil, err
+	}
+	rng, err := spn.Range(m.Converter)
+	if err != nil {
+		return nil, err
+	}
+	ident, err := source.Identifier(ctx, view, f, rng.Start)
+	if err != nil {
+		return nil, err
+	}
+	decSpan, err := ident.Declaration.Range.Span()
+	if err != nil {
+		return nil, err
+	}
+	_, decM, err := newColumnMap(ctx, view, decSpan.URI())
+	if err != nil {
+		return nil, err
+	}
+	loc, err := decM.Location(decSpan)
+	if err != nil {
+		return nil, err
+	}
+	return []protocol.Location{loc}, nil
+}
+
+func (s *Server) typeDefinition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) {
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	f, m, err := newColumnMap(ctx, view, uri)
+	if err != nil {
+		return nil, err
+	}
+	spn, err := m.PointSpan(params.Position)
+	if err != nil {
+		return nil, err
+	}
+	rng, err := spn.Range(m.Converter)
+	if err != nil {
+		return nil, err
+	}
+	ident, err := source.Identifier(ctx, view, f, rng.Start)
+	if err != nil {
+		return nil, err
+	}
+	identSpan, err := ident.Type.Range.Span()
+	if err != nil {
+		return nil, err
+	}
+	_, identM, err := newColumnMap(ctx, view, identSpan.URI())
+	if err != nil {
+		return nil, err
+	}
+	loc, err := identM.Location(identSpan)
+	if err != nil {
+		return nil, err
+	}
+	return []protocol.Location{loc}, nil
+}
diff --git a/internal/lsp/format.go b/internal/lsp/format.go
index 0277f5c..409d048 100644
--- a/internal/lsp/format.go
+++ b/internal/lsp/format.go
@@ -1,3 +1,7 @@
+// 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 (
@@ -9,6 +13,27 @@
 	"golang.org/x/tools/internal/span"
 )
 
+func (s *Server) formatting(ctx context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) {
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	spn := span.New(uri, span.Point{}, span.Point{})
+	return formatRange(ctx, view, spn)
+}
+
+func (s *Server) rangeFormatting(ctx context.Context, params *protocol.DocumentRangeFormattingParams) ([]protocol.TextEdit, error) {
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	_, m, err := newColumnMap(ctx, view, uri)
+	if err != nil {
+		return nil, err
+	}
+	spn, err := m.RangeSpan(params.Range)
+	if err != nil {
+		return nil, err
+	}
+	return formatRange(ctx, view, spn)
+}
+
 // formatRange formats a document with a given range.
 func formatRange(ctx context.Context, v source.View, s span.Span) ([]protocol.TextEdit, error) {
 	f, m, err := newColumnMap(ctx, v, s.URI())
@@ -69,16 +94,3 @@
 	}
 	return result, nil
 }
-
-func newColumnMap(ctx context.Context, v source.View, uri span.URI) (source.File, *protocol.ColumnMapper, error) {
-	f, err := v.GetFile(ctx, uri)
-	if err != nil {
-		return nil, nil, err
-	}
-	tok := f.GetToken(ctx)
-	if tok == nil {
-		return nil, nil, fmt.Errorf("no file information for %v", f.URI())
-	}
-	m := protocol.NewColumnMapper(f.URI(), f.GetFileSet(ctx), tok, f.GetContent(ctx))
-	return f, m, nil
-}
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
new file mode 100644
index 0000000..ffe5da6
--- /dev/null
+++ b/internal/lsp/general.go
@@ -0,0 +1,190 @@
+// 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 lsp
+
+import (
+	"context"
+	"fmt"
+	"go/ast"
+	"go/parser"
+	"go/token"
+	"os"
+	"path"
+	"strings"
+
+	"golang.org/x/tools/go/packages"
+	"golang.org/x/tools/internal/jsonrpc2"
+	"golang.org/x/tools/internal/lsp/cache"
+	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/span"
+)
+
+func (s *Server) initialize(ctx context.Context, params *protocol.InitializeParams) (*protocol.InitializeResult, error) {
+	s.initializedMu.Lock()
+	defer s.initializedMu.Unlock()
+	if s.isInitialized {
+		return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidRequest, "server already initialized")
+	}
+	s.isInitialized = true // mark server as initialized now
+
+	// TODO(rstambler): Change this default to protocol.Incremental (or add a
+	// flag). Disabled for now to simplify debugging.
+	s.textDocumentSyncKind = protocol.Full
+
+	s.setClientCapabilities(params.Capabilities)
+
+	// We need a "detached" context so it does not get timeout cancelled.
+	// TODO(iancottrell): Do we need to copy any values across?
+	viewContext := context.Background()
+	folders := params.WorkspaceFolders
+	if len(folders) == 0 {
+		if params.RootURI != "" {
+			folders = []protocol.WorkspaceFolder{{
+				URI:  params.RootURI,
+				Name: path.Base(params.RootURI),
+			}}
+		} else {
+			// no folders and no root, single file mode
+			//TODO(iancottrell): not sure how to do single file mode yet
+			//issue: golang.org/issue/31168
+			return nil, fmt.Errorf("single file mode not supported yet")
+		}
+	}
+	for _, folder := range folders {
+		uri := span.NewURI(folder.URI)
+		folderPath, err := uri.Filename()
+		if err != nil {
+			return nil, err
+		}
+		s.views = append(s.views, cache.NewView(viewContext, s.log, folder.Name, uri, &packages.Config{
+			Context: ctx,
+			Dir:     folderPath,
+			Env:     os.Environ(),
+			Mode:    packages.LoadImports,
+			Fset:    token.NewFileSet(),
+			Overlay: make(map[string][]byte),
+			ParseFile: func(fset *token.FileSet, filename string, src []byte) (*ast.File, error) {
+				return parser.ParseFile(fset, filename, src, parser.AllErrors|parser.ParseComments)
+			},
+			Tests: true,
+		}))
+	}
+
+	return &protocol.InitializeResult{
+		Capabilities: protocol.ServerCapabilities{
+			CodeActionProvider: true,
+			CompletionProvider: &protocol.CompletionOptions{
+				TriggerCharacters: []string{"."},
+			},
+			DefinitionProvider:              true,
+			DocumentFormattingProvider:      true,
+			DocumentRangeFormattingProvider: true,
+			DocumentSymbolProvider:          true,
+			HoverProvider:                   true,
+			DocumentHighlightProvider:       true,
+			SignatureHelpProvider: &protocol.SignatureHelpOptions{
+				TriggerCharacters: []string{"(", ","},
+			},
+			TextDocumentSync: &protocol.TextDocumentSyncOptions{
+				Change:    s.textDocumentSyncKind,
+				OpenClose: true,
+			},
+			TypeDefinitionProvider: true,
+		},
+	}, nil
+}
+
+func (s *Server) setClientCapabilities(caps protocol.ClientCapabilities) {
+	// Check if the client supports snippets in completion items.
+	s.snippetsSupported = caps.TextDocument.Completion.CompletionItem.SnippetSupport
+	// Check if the client supports configuration messages.
+	s.configurationSupported = caps.Workspace.Configuration
+	s.dynamicConfigurationSupported = caps.Workspace.DidChangeConfiguration.DynamicRegistration
+}
+
+func (s *Server) initialized(ctx context.Context, params *protocol.InitializedParams) error {
+	if s.configurationSupported {
+		if s.dynamicConfigurationSupported {
+			s.client.RegisterCapability(ctx, &protocol.RegistrationParams{
+				Registrations: []protocol.Registration{{
+					ID:     "workspace/didChangeConfiguration",
+					Method: "workspace/didChangeConfiguration",
+				}},
+			})
+		}
+		for _, view := range s.views {
+			config, err := s.client.Configuration(ctx, &protocol.ConfigurationParams{
+				Items: []protocol.ConfigurationItem{{
+					ScopeURI: protocol.NewURI(view.Folder),
+					Section:  "gopls",
+				}},
+			})
+			if err != nil {
+				return err
+			}
+			if err := s.processConfig(view, config[0]); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+func (s *Server) processConfig(view *cache.View, config interface{}) error {
+	// TODO: We should probably store and process more of the config.
+	if config == nil {
+		return nil // ignore error if you don't have a config
+	}
+	c, ok := config.(map[string]interface{})
+	if !ok {
+		return fmt.Errorf("invalid config gopls type %T", config)
+	}
+	// Get the environment for the go/packages config.
+	if env := c["env"]; env != nil {
+		menv, ok := env.(map[string]interface{})
+		if !ok {
+			return fmt.Errorf("invalid config gopls.env type %T", env)
+		}
+		for k, v := range menv {
+			view.Config.Env = applyEnv(view.Config.Env, k, v)
+		}
+	}
+	// Check if placeholders are enabled.
+	if usePlaceholders, ok := c["usePlaceholders"].(bool); ok {
+		s.usePlaceholders = usePlaceholders
+	}
+	return nil
+}
+
+func applyEnv(env []string, k string, v interface{}) []string {
+	prefix := k + "="
+	value := prefix + fmt.Sprint(v)
+	for i, s := range env {
+		if strings.HasPrefix(s, prefix) {
+			env[i] = value
+			return env
+		}
+	}
+	return append(env, value)
+}
+
+func (s *Server) shutdown(ctx context.Context) error {
+	// TODO(rstambler): Cancel contexts here?
+	s.initializedMu.Lock()
+	defer s.initializedMu.Unlock()
+	if !s.isInitialized {
+		return jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidRequest, "server not initialized")
+	}
+	s.isInitialized = false
+	return nil
+}
+
+func (s *Server) exit(ctx context.Context) error {
+	if s.isInitialized {
+		os.Exit(1)
+	}
+	os.Exit(0)
+	return nil
+}
diff --git a/internal/lsp/highlight.go b/internal/lsp/highlight.go
index 403ac28..288587d 100644
--- a/internal/lsp/highlight.go
+++ b/internal/lsp/highlight.go
@@ -5,10 +5,32 @@
 package lsp
 
 import (
+	"context"
+
 	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/span"
 )
 
+func (s *Server) documentHighlight(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.DocumentHighlight, error) {
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	f, m, err := newColumnMap(ctx, view, uri)
+	if err != nil {
+		return nil, err
+	}
+	spn, err := m.PointSpan(params.Position)
+	if err != nil {
+		return nil, err
+	}
+	rng, err := spn.Range(m.Converter)
+	if err != nil {
+		return nil, err
+	}
+	spans := source.Highlight(ctx, f, rng.Start)
+	return toProtocolHighlight(m, spans), nil
+}
+
 func toProtocolHighlight(m *protocol.ColumnMapper, spans []span.Span) []protocol.DocumentHighlight {
 	result := make([]protocol.DocumentHighlight, 0, len(spans))
 	kind := protocol.Text
diff --git a/internal/lsp/hover.go b/internal/lsp/hover.go
new file mode 100644
index 0000000..8348251
--- /dev/null
+++ b/internal/lsp/hover.go
@@ -0,0 +1,54 @@
+// 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 lsp
+
+import (
+	"context"
+
+	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/lsp/source"
+	"golang.org/x/tools/internal/span"
+)
+
+func (s *Server) hover(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.Hover, error) {
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	f, m, err := newColumnMap(ctx, view, uri)
+	if err != nil {
+		return nil, err
+	}
+	spn, err := m.PointSpan(params.Position)
+	if err != nil {
+		return nil, err
+	}
+	identRange, err := spn.Range(m.Converter)
+	if err != nil {
+		return nil, err
+	}
+	ident, err := source.Identifier(ctx, view, f, identRange.Start)
+	if err != nil {
+		return nil, err
+	}
+	content, err := ident.Hover(ctx, nil)
+	if err != nil {
+		return nil, err
+	}
+	markdown := "```go\n" + content + "\n```"
+	identSpan, err := ident.Range.Span()
+	if err != nil {
+		return nil, err
+	}
+	rng, err := m.Range(identSpan)
+	if err != nil {
+		return nil, err
+	}
+	return &protocol.Hover{
+		Contents: protocol.MarkupContent{
+			Kind:  protocol.Markdown,
+			Value: markdown,
+		},
+		Range: &rng,
+	}, nil
+}
diff --git a/internal/lsp/protocol/server.go b/internal/lsp/protocol/server.go
index 3344b53..6eb7747 100644
--- a/internal/lsp/protocol/server.go
+++ b/internal/lsp/protocol/server.go
@@ -20,7 +20,7 @@
 	DidChangeWorkspaceFolders(context.Context, *DidChangeWorkspaceFoldersParams) error
 	DidChangeConfiguration(context.Context, *DidChangeConfigurationParams) error
 	DidChangeWatchedFiles(context.Context, *DidChangeWatchedFilesParams) error
-	Symbols(context.Context, *WorkspaceSymbolParams) ([]SymbolInformation, error)
+	Symbol(context.Context, *WorkspaceSymbolParams) ([]SymbolInformation, error)
 	ExecuteCommand(context.Context, *ExecuteCommandParams) (interface{}, error)
 	DidOpen(context.Context, *DidOpenTextDocumentParams) error
 	DidChange(context.Context, *DidChangeTextDocumentParams) error
@@ -140,7 +140,7 @@
 				sendParseError(ctx, log, conn, r, err)
 				return
 			}
-			resp, err := server.Symbols(ctx, &params)
+			resp, err := server.Symbol(ctx, &params)
 			if err := conn.Reply(ctx, r, resp, err); err != nil {
 				log.Errorf(ctx, "%v", err)
 			}
@@ -502,7 +502,7 @@
 	return s.Conn.Notify(ctx, "workspace/didChangeWatchedFiles", params)
 }
 
-func (s *serverDispatcher) Symbols(ctx context.Context, params *WorkspaceSymbolParams) ([]SymbolInformation, error) {
+func (s *serverDispatcher) Symbol(ctx context.Context, params *WorkspaceSymbolParams) ([]SymbolInformation, error) {
 	var result []SymbolInformation
 	if err := s.Conn.Call(ctx, "workspace/symbol", params, &result); err != nil {
 		return nil, err
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 092df56..326d191 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -5,19 +5,11 @@
 package lsp
 
 import (
-	"bytes"
 	"context"
 	"fmt"
-	"go/ast"
-	"go/parser"
-	"go/token"
 	"net"
-	"os"
-	"path"
-	"strings"
 	"sync"
 
-	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/internal/jsonrpc2"
 	"golang.org/x/tools/internal/lsp/cache"
 	"golang.org/x/tools/internal/lsp/protocol"
@@ -67,13 +59,17 @@
 	}
 }
 
+func (s *Server) Run(ctx context.Context) error {
+	return s.Conn.Run(ctx)
+}
+
 type Server struct {
 	Conn   *jsonrpc2.Conn
 	client protocol.Client
 	log    xlog.Logger
 
 	initializedMu sync.Mutex
-	initialized   bool // set once the server has received "initialize" request
+	isInitialized bool // set once the server has received "initialize" request
 
 	// Configurations.
 	// TODO(rstambler): Separate these into their own struct?
@@ -92,139 +88,26 @@
 	undelivered   map[span.URI][]source.Diagnostic
 }
 
-func (s *Server) Run(ctx context.Context) error {
-	return s.Conn.Run(ctx)
-}
+// General
 
 func (s *Server) Initialize(ctx context.Context, params *protocol.InitializeParams) (*protocol.InitializeResult, error) {
-	s.initializedMu.Lock()
-	defer s.initializedMu.Unlock()
-	if s.initialized {
-		return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidRequest, "server already initialized")
-	}
-	s.initialized = true // mark server as initialized now
-
-	// TODO(rstambler): Change this default to protocol.Incremental (or add a
-	// flag). Disabled for now to simplify debugging.
-	s.textDocumentSyncKind = protocol.Full
-
-	s.setClientCapabilities(params.Capabilities)
-
-	// We need a "detached" context so it does not get timeout cancelled.
-	// TODO(iancottrell): Do we need to copy any values across?
-	viewContext := context.Background()
-	folders := params.WorkspaceFolders
-	if len(folders) == 0 {
-		if params.RootURI != "" {
-			folders = []protocol.WorkspaceFolder{{
-				URI:  params.RootURI,
-				Name: path.Base(params.RootURI),
-			}}
-		} else {
-			// no folders and no root, single file mode
-			//TODO(iancottrell): not sure how to do single file mode yet
-			//issue: golang.org/issue/31168
-			return nil, fmt.Errorf("single file mode not supported yet")
-		}
-	}
-	for _, folder := range folders {
-		uri := span.NewURI(folder.URI)
-		folderPath, err := uri.Filename()
-		if err != nil {
-			return nil, err
-		}
-		s.views = append(s.views, cache.NewView(viewContext, s.log, folder.Name, uri, &packages.Config{
-			Context: ctx,
-			Dir:     folderPath,
-			Env:     os.Environ(),
-			Mode:    packages.LoadImports,
-			Fset:    token.NewFileSet(),
-			Overlay: make(map[string][]byte),
-			ParseFile: func(fset *token.FileSet, filename string, src []byte) (*ast.File, error) {
-				return parser.ParseFile(fset, filename, src, parser.AllErrors|parser.ParseComments)
-			},
-			Tests: true,
-		}))
-	}
-
-	return &protocol.InitializeResult{
-		Capabilities: protocol.ServerCapabilities{
-			CodeActionProvider: true,
-			CompletionProvider: &protocol.CompletionOptions{
-				TriggerCharacters: []string{"."},
-			},
-			DefinitionProvider:              true,
-			DocumentFormattingProvider:      true,
-			DocumentRangeFormattingProvider: true,
-			DocumentSymbolProvider:          true,
-			HoverProvider:                   true,
-			DocumentHighlightProvider:       true,
-			SignatureHelpProvider: &protocol.SignatureHelpOptions{
-				TriggerCharacters: []string{"(", ","},
-			},
-			TextDocumentSync: &protocol.TextDocumentSyncOptions{
-				Change:    s.textDocumentSyncKind,
-				OpenClose: true,
-			},
-			TypeDefinitionProvider: true,
-		},
-	}, nil
-}
-
-func (s *Server) setClientCapabilities(caps protocol.ClientCapabilities) {
-	// Check if the client supports snippets in completion items.
-	s.snippetsSupported = caps.TextDocument.Completion.CompletionItem.SnippetSupport
-	// Check if the client supports configuration messages.
-	s.configurationSupported = caps.Workspace.Configuration
-	s.dynamicConfigurationSupported = caps.Workspace.DidChangeConfiguration.DynamicRegistration
+	return s.initialize(ctx, params)
 }
 
 func (s *Server) Initialized(ctx context.Context, params *protocol.InitializedParams) error {
-	if s.configurationSupported {
-		if s.dynamicConfigurationSupported {
-			s.client.RegisterCapability(ctx, &protocol.RegistrationParams{
-				Registrations: []protocol.Registration{{
-					ID:     "workspace/didChangeConfiguration",
-					Method: "workspace/didChangeConfiguration",
-				}},
-			})
-		}
-		for _, view := range s.views {
-			config, err := s.client.Configuration(ctx, &protocol.ConfigurationParams{
-				Items: []protocol.ConfigurationItem{{
-					ScopeURI: protocol.NewURI(view.Folder),
-					Section:  "gopls",
-				}},
-			})
-			if err != nil {
-				return err
-			}
-			if err := s.processConfig(view, config[0]); err != nil {
-				return err
-			}
-		}
-	}
-	return nil
+	return s.initialized(ctx, params)
 }
 
-func (s *Server) Shutdown(context.Context) error {
-	s.initializedMu.Lock()
-	defer s.initializedMu.Unlock()
-	if !s.initialized {
-		return jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidRequest, "server not initialized")
-	}
-	s.initialized = false
-	return nil
+func (s *Server) Shutdown(ctx context.Context) error {
+	return s.shutdown(ctx)
 }
 
 func (s *Server) Exit(ctx context.Context) error {
-	if s.initialized {
-		os.Exit(1)
-	}
-	os.Exit(0)
-	return nil
+	return s.exit(ctx)
 }
 
+// Workspace
+
 func (s *Server) DidChangeWorkspaceFolders(context.Context, *protocol.DidChangeWorkspaceFoldersParams) error {
 	return notImplemented("DidChangeWorkspaceFolders")
 }
@@ -237,78 +120,22 @@
 	return notImplemented("DidChangeWatchedFiles")
 }
 
-func (s *Server) Symbols(context.Context, *protocol.WorkspaceSymbolParams) ([]protocol.SymbolInformation, error) {
-	return nil, notImplemented("Symbols")
+func (s *Server) Symbol(context.Context, *protocol.WorkspaceSymbolParams) ([]protocol.SymbolInformation, error) {
+	return nil, notImplemented("Symbol")
 }
 
 func (s *Server) ExecuteCommand(context.Context, *protocol.ExecuteCommandParams) (interface{}, error) {
 	return nil, notImplemented("ExecuteCommand")
 }
 
+// Text Synchronization
+
 func (s *Server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error {
 	return s.cacheAndDiagnose(ctx, span.NewURI(params.TextDocument.URI), params.TextDocument.Text)
 }
 
-func (s *Server) applyChanges(ctx context.Context, params *protocol.DidChangeTextDocumentParams) (string, error) {
-	if len(params.ContentChanges) == 1 && params.ContentChanges[0].Range == nil {
-		// If range is empty, we expect the full content of file, i.e. a single change with no range.
-		change := params.ContentChanges[0]
-		if change.RangeLength != 0 {
-			return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "unexpected change range provided")
-		}
-		return change.Text, nil
-	}
-
-	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
-	file, m, err := newColumnMap(ctx, view, uri)
-	if err != nil {
-		return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found")
-	}
-	content := file.GetContent(ctx)
-	for _, change := range params.ContentChanges {
-		spn, err := m.RangeSpan(*change.Range)
-		if err != nil {
-			return "", err
-		}
-		if !spn.HasOffset() {
-			return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "invalid range for content change")
-		}
-		start, end := spn.Start().Offset(), spn.End().Offset()
-		if end <= start {
-			return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "invalid range for content change")
-		}
-		var buf bytes.Buffer
-		buf.Write(content[:start])
-		buf.WriteString(change.Text)
-		buf.Write(content[end:])
-		content = buf.Bytes()
-	}
-	return string(content), nil
-}
-
 func (s *Server) DidChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error {
-	if len(params.ContentChanges) < 1 {
-		return jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "no content changes provided")
-	}
-
-	var text string
-	switch s.textDocumentSyncKind {
-	case protocol.Incremental:
-		var err error
-		text, err = s.applyChanges(ctx, params)
-		if err != nil {
-			return err
-		}
-	case protocol.Full:
-		// We expect the full content of file, i.e. a single change with no range.
-		change := params.ContentChanges[0]
-		if change.RangeLength != 0 {
-			return jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "unexpected change range provided")
-		}
-		text = change.Text
-	}
-	return s.cacheAndDiagnose(ctx, span.NewURI(params.TextDocument.URI), text)
+	return s.didChange(ctx, params)
 }
 
 func (s *Server) WillSave(context.Context, *protocol.WillSaveTextDocumentParams) error {
@@ -319,16 +146,16 @@
 	return nil, notImplemented("WillSaveWaitUntil")
 }
 
-func (s *Server) DidSave(context.Context, *protocol.DidSaveTextDocumentParams) error {
-	return nil // ignore
+func (s *Server) DidSave(ctx context.Context, params *protocol.DidSaveTextDocumentParams) error {
+	return s.didSave(ctx, params)
 }
 
 func (s *Server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
-	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
-	return view.SetContent(ctx, uri, nil)
+	return s.didClose(ctx, params)
 }
 
+// Language Features
+
 func (s *Server) Completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) {
 	return s.completion(ctx, params)
 }
@@ -338,134 +165,19 @@
 }
 
 func (s *Server) Hover(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.Hover, error) {
-	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
-	f, m, err := newColumnMap(ctx, view, uri)
-	if err != nil {
-		return nil, err
-	}
-	spn, err := m.PointSpan(params.Position)
-	if err != nil {
-		return nil, err
-	}
-	identRange, err := spn.Range(m.Converter)
-	if err != nil {
-		return nil, err
-	}
-	ident, err := source.Identifier(ctx, view, f, identRange.Start)
-	if err != nil {
-		return nil, err
-	}
-	content, err := ident.Hover(ctx, nil)
-	if err != nil {
-		return nil, err
-	}
-	markdown := "```go\n" + content + "\n```"
-	identSpan, err := ident.Range.Span()
-	if err != nil {
-		return nil, err
-	}
-	rng, err := m.Range(identSpan)
-	if err != nil {
-		return nil, err
-	}
-	return &protocol.Hover{
-		Contents: protocol.MarkupContent{
-			Kind:  protocol.Markdown,
-			Value: markdown,
-		},
-		Range: &rng,
-	}, nil
+	return s.hover(ctx, params)
 }
 
 func (s *Server) SignatureHelp(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.SignatureHelp, error) {
-	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
-	f, m, err := newColumnMap(ctx, view, uri)
-	if err != nil {
-		return nil, err
-	}
-	spn, err := m.PointSpan(params.Position)
-	if err != nil {
-		return nil, err
-	}
-	rng, err := spn.Range(m.Converter)
-	if err != nil {
-		return nil, err
-	}
-	info, err := source.SignatureHelp(ctx, f, rng.Start)
-	if err != nil {
-		s.log.Infof(ctx, "no signature help for %s:%v:%v : %s", uri, int(params.Position.Line), int(params.Position.Character), err)
-	}
-	return toProtocolSignatureHelp(info), nil
+	return s.signatureHelp(ctx, params)
 }
 
 func (s *Server) Definition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) {
-	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
-	f, m, err := newColumnMap(ctx, view, uri)
-	if err != nil {
-		return nil, err
-	}
-	spn, err := m.PointSpan(params.Position)
-	if err != nil {
-		return nil, err
-	}
-	rng, err := spn.Range(m.Converter)
-	if err != nil {
-		return nil, err
-	}
-	ident, err := source.Identifier(ctx, view, f, rng.Start)
-	if err != nil {
-		return nil, err
-	}
-	decSpan, err := ident.Declaration.Range.Span()
-	if err != nil {
-		return nil, err
-	}
-	_, decM, err := newColumnMap(ctx, view, decSpan.URI())
-	if err != nil {
-		return nil, err
-	}
-	loc, err := decM.Location(decSpan)
-	if err != nil {
-		return nil, err
-	}
-	return []protocol.Location{loc}, nil
+	return s.definition(ctx, params)
 }
 
 func (s *Server) TypeDefinition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) {
-	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
-	f, m, err := newColumnMap(ctx, view, uri)
-	if err != nil {
-		return nil, err
-	}
-	spn, err := m.PointSpan(params.Position)
-	if err != nil {
-		return nil, err
-	}
-	rng, err := spn.Range(m.Converter)
-	if err != nil {
-		return nil, err
-	}
-	ident, err := source.Identifier(ctx, view, f, rng.Start)
-	if err != nil {
-		return nil, err
-	}
-	identSpan, err := ident.Type.Range.Span()
-	if err != nil {
-		return nil, err
-	}
-	_, identM, err := newColumnMap(ctx, view, identSpan.URI())
-	if err != nil {
-		return nil, err
-	}
-	loc, err := identM.Location(identSpan)
-	if err != nil {
-		return nil, err
-	}
-	return []protocol.Location{loc}, nil
+	return s.typeDefinition(ctx, params)
 }
 
 func (s *Server) Implementation(context.Context, *protocol.TextDocumentPositionParams) ([]protocol.Location, error) {
@@ -477,33 +189,11 @@
 }
 
 func (s *Server) DocumentHighlight(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.DocumentHighlight, error) {
-	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
-	f, m, err := newColumnMap(ctx, view, uri)
-	if err != nil {
-		return nil, err
-	}
-	spn, err := m.PointSpan(params.Position)
-	if err != nil {
-		return nil, err
-	}
-	rng, err := spn.Range(m.Converter)
-	if err != nil {
-		return nil, err
-	}
-	spans := source.Highlight(ctx, f, rng.Start)
-	return toProtocolHighlight(m, spans), nil
+	return s.documentHighlight(ctx, params)
 }
 
 func (s *Server) DocumentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]protocol.DocumentSymbol, error) {
-	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
-	f, m, err := newColumnMap(ctx, view, uri)
-	if err != nil {
-		return nil, err
-	}
-	symbols := source.DocumentSymbols(ctx, f)
-	return toProtocolDocumentSymbols(m, symbols), nil
+	return s.documentSymbol(ctx, params)
 }
 
 func (s *Server) CodeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
@@ -535,24 +225,11 @@
 }
 
 func (s *Server) Formatting(ctx context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) {
-	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
-	spn := span.New(uri, span.Point{}, span.Point{})
-	return formatRange(ctx, view, spn)
+	return s.formatting(ctx, params)
 }
 
 func (s *Server) RangeFormatting(ctx context.Context, params *protocol.DocumentRangeFormattingParams) ([]protocol.TextEdit, error) {
-	uri := span.NewURI(params.TextDocument.URI)
-	view := s.findView(ctx, uri)
-	_, m, err := newColumnMap(ctx, view, uri)
-	if err != nil {
-		return nil, err
-	}
-	spn, err := m.RangeSpan(params.Range)
-	if err != nil {
-		return nil, err
-	}
-	return formatRange(ctx, view, spn)
+	return s.rangeFormatting(ctx, params)
 }
 
 func (s *Server) OnTypeFormatting(context.Context, *protocol.DocumentOnTypeFormattingParams) ([]protocol.TextEdit, error) {
@@ -567,67 +244,6 @@
 	return nil, notImplemented("FoldingRanges")
 }
 
-func (s *Server) processConfig(view *cache.View, config interface{}) error {
-	// TODO: We should probably store and process more of the config.
-	if config == nil {
-		return nil // ignore error if you don't have a config
-	}
-	c, ok := config.(map[string]interface{})
-	if !ok {
-		return fmt.Errorf("invalid config gopls type %T", config)
-	}
-	// Get the environment for the go/packages config.
-	if env := c["env"]; env != nil {
-		menv, ok := env.(map[string]interface{})
-		if !ok {
-			return fmt.Errorf("invalid config gopls.env type %T", env)
-		}
-		for k, v := range menv {
-			view.Config.Env = applyEnv(view.Config.Env, k, v)
-		}
-	}
-	// Check if placeholders are enabled.
-	if usePlaceholders, ok := c["usePlaceholders"].(bool); ok {
-		s.usePlaceholders = usePlaceholders
-	}
-	return nil
-}
-
-func applyEnv(env []string, k string, v interface{}) []string {
-	prefix := k + "="
-	value := prefix + fmt.Sprint(v)
-	for i, s := range env {
-		if strings.HasPrefix(s, prefix) {
-			env[i] = value
-			return env
-		}
-	}
-	return append(env, value)
-}
-
 func notImplemented(method string) *jsonrpc2.Error {
 	return jsonrpc2.NewErrorf(jsonrpc2.CodeMethodNotFound, "method %q not yet implemented", method)
 }
-
-func (s *Server) findView(ctx context.Context, uri span.URI) *cache.View {
-	// first see if a view already has this file
-	for _, view := range s.views {
-		if view.FindFile(ctx, uri) != nil {
-			return view
-		}
-	}
-	var longest *cache.View
-	for _, view := range s.views {
-		if longest != nil && len(longest.Folder) > len(view.Folder) {
-			continue
-		}
-		if strings.HasPrefix(string(uri), string(view.Folder)) {
-			longest = view
-		}
-	}
-	if longest != nil {
-		return longest
-	}
-	//TODO: are there any more heuristics we can use?
-	return s.views[0]
-}
diff --git a/internal/lsp/signature_help.go b/internal/lsp/signature_help.go
index 7479db4..7471321 100644
--- a/internal/lsp/signature_help.go
+++ b/internal/lsp/signature_help.go
@@ -5,10 +5,35 @@
 package lsp
 
 import (
+	"context"
+
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
+	"golang.org/x/tools/internal/span"
 )
 
+func (s *Server) signatureHelp(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.SignatureHelp, error) {
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	f, m, err := newColumnMap(ctx, view, uri)
+	if err != nil {
+		return nil, err
+	}
+	spn, err := m.PointSpan(params.Position)
+	if err != nil {
+		return nil, err
+	}
+	rng, err := spn.Range(m.Converter)
+	if err != nil {
+		return nil, err
+	}
+	info, err := source.SignatureHelp(ctx, f, rng.Start)
+	if err != nil {
+		s.log.Infof(ctx, "no signature help for %s:%v:%v : %s", uri, int(params.Position.Line), int(params.Position.Character), err)
+	}
+	return toProtocolSignatureHelp(info), nil
+}
+
 func toProtocolSignatureHelp(info *source.SignatureInformation) *protocol.SignatureHelp {
 	if info == nil {
 		return &protocol.SignatureHelp{}
diff --git a/internal/lsp/symbols.go b/internal/lsp/symbols.go
index 6ac09e0..da57ec2 100644
--- a/internal/lsp/symbols.go
+++ b/internal/lsp/symbols.go
@@ -5,10 +5,24 @@
 package lsp
 
 import (
+	"context"
+
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
+	"golang.org/x/tools/internal/span"
 )
 
+func (s *Server) documentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]protocol.DocumentSymbol, error) {
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	f, m, err := newColumnMap(ctx, view, uri)
+	if err != nil {
+		return nil, err
+	}
+	symbols := source.DocumentSymbols(ctx, f)
+	return toProtocolDocumentSymbols(m, symbols), nil
+}
+
 func toProtocolDocumentSymbols(m *protocol.ColumnMapper, symbols []source.Symbol) []protocol.DocumentSymbol {
 	result := make([]protocol.DocumentSymbol, 0, len(symbols))
 	for _, s := range symbols {
diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go
new file mode 100644
index 0000000..f7f6074
--- /dev/null
+++ b/internal/lsp/text_synchronization.go
@@ -0,0 +1,86 @@
+// 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 lsp
+
+import (
+	"bytes"
+	"context"
+
+	"golang.org/x/tools/internal/jsonrpc2"
+	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/span"
+)
+
+func (s *Server) didChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error {
+	if len(params.ContentChanges) < 1 {
+		return jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "no content changes provided")
+	}
+
+	var text string
+	switch s.textDocumentSyncKind {
+	case protocol.Incremental:
+		var err error
+		text, err = s.applyChanges(ctx, params)
+		if err != nil {
+			return err
+		}
+	case protocol.Full:
+		// We expect the full content of file, i.e. a single change with no range.
+		change := params.ContentChanges[0]
+		if change.RangeLength != 0 {
+			return jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "unexpected change range provided")
+		}
+		text = change.Text
+	}
+	return s.cacheAndDiagnose(ctx, span.NewURI(params.TextDocument.URI), text)
+}
+
+func (s *Server) applyChanges(ctx context.Context, params *protocol.DidChangeTextDocumentParams) (string, error) {
+	if len(params.ContentChanges) == 1 && params.ContentChanges[0].Range == nil {
+		// If range is empty, we expect the full content of file, i.e. a single change with no range.
+		change := params.ContentChanges[0]
+		if change.RangeLength != 0 {
+			return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "unexpected change range provided")
+		}
+		return change.Text, nil
+	}
+
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	file, m, err := newColumnMap(ctx, view, uri)
+	if err != nil {
+		return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found")
+	}
+	content := file.GetContent(ctx)
+	for _, change := range params.ContentChanges {
+		spn, err := m.RangeSpan(*change.Range)
+		if err != nil {
+			return "", err
+		}
+		if !spn.HasOffset() {
+			return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "invalid range for content change")
+		}
+		start, end := spn.Start().Offset(), spn.End().Offset()
+		if end <= start {
+			return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "invalid range for content change")
+		}
+		var buf bytes.Buffer
+		buf.Write(content[:start])
+		buf.WriteString(change.Text)
+		buf.Write(content[end:])
+		content = buf.Bytes()
+	}
+	return string(content), nil
+}
+
+func (s *Server) didSave(ctx context.Context, params *protocol.DidSaveTextDocumentParams) error {
+	return nil // ignore
+}
+
+func (s *Server) didClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
+	uri := span.NewURI(params.TextDocument.URI)
+	view := s.findView(ctx, uri)
+	return view.SetContent(ctx, uri, nil)
+}
diff --git a/internal/lsp/util.go b/internal/lsp/util.go
new file mode 100644
index 0000000..e1c97ad
--- /dev/null
+++ b/internal/lsp/util.go
@@ -0,0 +1,54 @@
+// 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 lsp
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"golang.org/x/tools/internal/lsp/cache"
+	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/lsp/source"
+	"golang.org/x/tools/internal/span"
+)
+
+// findView returns the view corresponding to the given URI.
+// If the file is not already associated with a view, pick one using some heuristics.
+func (s *Server) findView(ctx context.Context, uri span.URI) *cache.View {
+	// first see if a view already has this file
+	for _, view := range s.views {
+		if view.FindFile(ctx, uri) != nil {
+			return view
+		}
+	}
+	var longest *cache.View
+	for _, view := range s.views {
+		if longest != nil && len(longest.Folder) > len(view.Folder) {
+			continue
+		}
+		if strings.HasPrefix(string(uri), string(view.Folder)) {
+			longest = view
+		}
+	}
+	if longest != nil {
+		return longest
+	}
+	//TODO: are there any more heuristics we can use?
+	return s.views[0]
+}
+
+func newColumnMap(ctx context.Context, v source.View, uri span.URI) (source.File, *protocol.ColumnMapper, error) {
+	f, err := v.GetFile(ctx, uri)
+	if err != nil {
+		return nil, nil, err
+	}
+	tok := f.GetToken(ctx)
+	if tok == nil {
+		return nil, nil, fmt.Errorf("no file information for %v", f.URI())
+	}
+	m := protocol.NewColumnMapper(f.URI(), f.GetFileSet(ctx), tok, f.GetContent(ctx))
+	return f, m, nil
+}