internal/lsp: prepare for non go files

This abstracts out the concrete file type so that we can support non go files.

Change-Id: I7447daa2ce076ec2867de9e59a0dedfe1a0553f5
Reviewed-on: https://go-review.googlesource.com/c/tools/+/175217
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/cache/check.go b/internal/lsp/cache/check.go
index 4a9ec80..90eb5a5 100644
--- a/internal/lsp/cache/check.go
+++ b/internal/lsp/cache/check.go
@@ -14,10 +14,11 @@
 
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/packages"
+	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/span"
 )
 
-func (v *view) parse(ctx context.Context, f *file) ([]packages.Error, error) {
+func (v *view) parse(ctx context.Context, file source.File) ([]packages.Error, error) {
 	v.mcache.mu.Lock()
 	defer v.mcache.mu.Unlock()
 
@@ -26,6 +27,11 @@
 		return nil, err
 	}
 
+	f, ok := file.(*goFile)
+	if !ok {
+		return nil, fmt.Errorf("not a go file: %v", file.URI())
+	}
+
 	// If the package for the file has not been invalidated by the application
 	// of the pending changes, there is no need to continue.
 	if f.isPopulated() {
@@ -37,7 +43,7 @@
 		return errs, err
 	}
 	if f.meta == nil {
-		return nil, fmt.Errorf("no metadata found for %v", f.filename)
+		return nil, fmt.Errorf("no metadata found for %v", f.filename())
 	}
 	imp := &importer{
 		view: v,
@@ -56,19 +62,19 @@
 
 	// If we still have not found the package for the file, something is wrong.
 	if f.pkg == nil {
-		return nil, fmt.Errorf("parse: no package found for %v", f.filename)
+		return nil, fmt.Errorf("parse: no package found for %v", f.filename())
 	}
 	return nil, nil
 }
 
-func (v *view) checkMetadata(ctx context.Context, f *file) ([]packages.Error, error) {
-	if v.reparseImports(ctx, f, f.filename) {
+func (v *view) checkMetadata(ctx context.Context, f *goFile) ([]packages.Error, error) {
+	if v.reparseImports(ctx, f, f.filename()) {
 		cfg := v.config
 		cfg.Mode = packages.LoadImports | packages.NeedTypesSizes
-		pkgs, err := packages.Load(&cfg, fmt.Sprintf("file=%s", f.filename))
+		pkgs, err := packages.Load(&cfg, fmt.Sprintf("file=%s", f.filename()))
 		if len(pkgs) == 0 {
 			if err == nil {
-				err = fmt.Errorf("no packages found for %s", f.filename)
+				err = fmt.Errorf("no packages found for %s", f.filename())
 			}
 			// Return this error as a diagnostic to the user.
 			return []packages.Error{
@@ -84,7 +90,7 @@
 			if len(pkg.Errors) > 0 {
 				return pkg.Errors, fmt.Errorf("package %s has errors, skipping type-checking", pkg.PkgPath)
 			}
-			v.link(pkg.PkgPath, pkg, nil)
+			v.link(ctx, pkg.PkgPath, pkg, nil)
 		}
 	}
 	return nil, nil
@@ -92,7 +98,7 @@
 
 // reparseImports reparses a file's import declarations to determine if they
 // have changed.
-func (v *view) reparseImports(ctx context.Context, f *file, filename string) bool {
+func (v *view) reparseImports(ctx context.Context, f *goFile, filename string) bool {
 	if f.meta == nil {
 		return true
 	}
@@ -113,7 +119,7 @@
 	return false
 }
 
-func (v *view) link(pkgPath string, pkg *packages.Package, parent *metadata) *metadata {
+func (v *view) link(ctx context.Context, pkgPath string, pkg *packages.Package, parent *metadata) *metadata {
 	m, ok := v.mcache.packages[pkgPath]
 	if !ok {
 		m = &metadata{
@@ -130,7 +136,12 @@
 	m.files = pkg.CompiledGoFiles
 	for _, filename := range m.files {
 		if f, _ := v.getFile(span.FileURI(filename)); f != nil {
-			f.meta = m
+			gof, ok := f.(*goFile)
+			if !ok {
+				v.Logger().Errorf(ctx, "not a go file: %v", f.URI())
+				continue
+			}
+			gof.meta = m
 		}
 	}
 	// Connect the import graph.
@@ -140,7 +151,7 @@
 	}
 	for importPath, importPkg := range pkg.Imports {
 		if _, ok := m.children[importPath]; !ok {
-			v.link(importPath, importPkg, m)
+			v.link(ctx, importPath, importPkg, m)
 		}
 	}
 	// Clear out any imports that have been removed.
@@ -273,10 +284,15 @@
 			v.Logger().Errorf(ctx, "no file: %v", err)
 			continue
 		}
-		f.token = tok
-		f.ast = file
-		f.imports = f.ast.Imports
-		f.pkg = pkg
+		gof, ok := f.(*goFile)
+		if !ok {
+			v.Logger().Errorf(ctx, "not a go file: %v", f.URI())
+			continue
+		}
+		gof.token = tok
+		gof.ast = file
+		gof.imports = gof.ast.Imports
+		gof.pkg = pkg
 	}
 
 	v.pcache.mu.Lock()
diff --git a/internal/lsp/cache/file.go b/internal/lsp/cache/file.go
index 1e69dfe..3711e96 100644
--- a/internal/lsp/cache/file.go
+++ b/internal/lsp/cache/file.go
@@ -16,17 +16,32 @@
 	"golang.org/x/tools/internal/span"
 )
 
-// file holds all the information we know about a file.
-type file struct {
-	uris     []span.URI
-	filename string
-	basename string
+// viewFile extends source.File with helper methods for the view package.
+type viewFile interface {
+	source.File
+	setContent(content []byte)
+	filename() string
+	addURI(uri span.URI) int
+	isActive() bool
+}
+
+// fileBase holds the common functionality for all files.
+// It is intended to be embedded in the file implementations
+type fileBase struct {
+	uris  []span.URI
+	fname string
 
 	view    *view
 	active  bool
 	content []byte
-	ast     *ast.File
 	token   *token.File
+}
+
+// goFile holds all the information we know about a go file.
+type goFile struct {
+	fileBase
+
+	ast     *ast.File
 	pkg     *pkg
 	meta    *metadata
 	imports []*ast.ImportSpec
@@ -36,17 +51,25 @@
 	return strings.ToLower(filepath.Base(filename))
 }
 
-func (f *file) URI() span.URI {
+func (f *fileBase) URI() span.URI {
 	return f.uris[0]
 }
 
+func (f *fileBase) filename() string {
+	return f.fname
+}
+
+func (f *fileBase) isActive() bool {
+	return f.active
+}
+
 // View returns the view associated with the file.
-func (f *file) View() source.View {
+func (f *fileBase) View() source.View {
 	return f.view
 }
 
 // GetContent returns the contents of the file, reading it from file system if needed.
-func (f *file) GetContent(ctx context.Context) []byte {
+func (f *fileBase) GetContent(ctx context.Context) []byte {
 	f.view.mu.Lock()
 	defer f.view.mu.Unlock()
 
@@ -57,14 +80,13 @@
 	return f.content
 }
 
-func (f *file) GetFileSet(ctx context.Context) *token.FileSet {
+func (f *fileBase) GetFileSet(ctx context.Context) *token.FileSet {
 	return f.view.config.Fset
 }
 
-func (f *file) GetToken(ctx context.Context) *token.File {
+func (f *goFile) GetToken(ctx context.Context) *token.File {
 	f.view.mu.Lock()
 	defer f.view.mu.Unlock()
-
 	if f.token == nil || len(f.view.contentChanges) > 0 {
 		if _, err := f.view.parse(ctx, f); err != nil {
 			return nil
@@ -73,7 +95,7 @@
 	return f.token
 }
 
-func (f *file) GetAST(ctx context.Context) *ast.File {
+func (f *goFile) GetAST(ctx context.Context) *ast.File {
 	f.view.mu.Lock()
 	defer f.view.mu.Unlock()
 
@@ -85,7 +107,7 @@
 	return f.ast
 }
 
-func (f *file) GetPackage(ctx context.Context) source.Package {
+func (f *goFile) GetPackage(ctx context.Context) source.Package {
 	f.view.mu.Lock()
 	defer f.view.mu.Unlock()
 
@@ -103,7 +125,7 @@
 
 // read is the internal part of GetContent. It assumes that the caller is
 // holding the mutex of the file's view.
-func (f *file) read(ctx context.Context) {
+func (f *fileBase) read(ctx context.Context) {
 	if f.content != nil {
 		if len(f.view.contentChanges) == 0 {
 			return
@@ -118,25 +140,25 @@
 		}
 	}
 	// We might have the content saved in an overlay.
-	if content, ok := f.view.config.Overlay[f.filename]; ok {
+	if content, ok := f.view.config.Overlay[f.filename()]; ok {
 		f.content = content
 		return
 	}
 	// We don't know the content yet, so read it.
-	content, err := ioutil.ReadFile(f.filename)
+	content, err := ioutil.ReadFile(f.filename())
 	if err != nil {
-		f.view.Logger().Errorf(ctx, "unable to read file %s: %v", f.filename, err)
+		f.view.Logger().Errorf(ctx, "unable to read file %s: %v", f.filename(), err)
 		return
 	}
 	f.content = content
 }
 
 // isPopulated returns true if all of the computed fields of the file are set.
-func (f *file) isPopulated() bool {
+func (f *goFile) isPopulated() bool {
 	return f.ast != nil && f.token != nil && f.pkg != nil && f.meta != nil && f.imports != nil
 }
 
-func (f *file) GetActiveReverseDeps(ctx context.Context) []source.File {
+func (f *goFile) GetActiveReverseDeps(ctx context.Context) []source.GoFile {
 	pkg := f.GetPackage(ctx)
 	if pkg == nil {
 		return nil
@@ -149,10 +171,10 @@
 	defer f.view.mcache.mu.Unlock()
 
 	seen := make(map[string]struct{}) // visited packages
-	results := make(map[*file]struct{})
+	results := make(map[*goFile]struct{})
 	f.view.reverseDeps(ctx, seen, results, pkg.PkgPath())
 
-	files := make([]source.File, 0, len(results))
+	files := make([]source.GoFile, 0, len(results))
 	for rd := range results {
 		if rd == nil {
 			continue
@@ -166,7 +188,7 @@
 	return files
 }
 
-func (v *view) reverseDeps(ctx context.Context, seen map[string]struct{}, results map[*file]struct{}, pkgPath string) {
+func (v *view) reverseDeps(ctx context.Context, seen map[string]struct{}, results map[*goFile]struct{}, pkgPath string) {
 	if _, ok := seen[pkgPath]; ok {
 		return
 	}
@@ -176,8 +198,8 @@
 		return
 	}
 	for _, filename := range m.files {
-		if f, err := v.getFile(span.FileURI(filename)); err == nil && f.active {
-			results[f] = struct{}{}
+		if f, err := v.getFile(span.FileURI(filename)); err == nil && f.isActive() {
+			results[f.(*goFile)] = struct{}{}
 		}
 	}
 	for parentPkgPath := range m.parents {
diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go
index 0cb67e0..b8264b5 100644
--- a/internal/lsp/cache/parse.go
+++ b/internal/lsp/cache/parse.go
@@ -50,7 +50,9 @@
 		}
 		var fAST *ast.File
 		if f != nil {
-			fAST = f.ast
+			if gof, ok := f.(*goFile); ok {
+				fAST = gof.ast
+			}
 		}
 
 		wg.Add(1)
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 8117065..15c054e 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -51,8 +51,8 @@
 
 	// keep track of files by uri and by basename, a single file may be mapped
 	// to multiple uris, and the same basename may map to multiple files
-	filesByURI  map[span.URI]*file
-	filesByBase map[string][]*file
+	filesByURI  map[span.URI]viewFile
+	filesByBase map[string][]viewFile
 
 	// contentChanges saves the content changes for a given state of the view.
 	// When type information is requested by the view, all of the dirty changes
@@ -105,8 +105,8 @@
 		config:         *config,
 		name:           name,
 		folder:         folder,
-		filesByURI:     make(map[span.URI]*file),
-		filesByBase:    make(map[string][]*file),
+		filesByURI:     make(map[span.URI]viewFile),
+		filesByBase:    make(map[string][]viewFile),
 		contentChanges: make(map[span.URI]func()),
 		mcache: &metadataCache{
 			packages: make(map[string]*metadata),
@@ -227,6 +227,10 @@
 	if err != nil {
 		return
 	}
+	f.setContent(content)
+}
+
+func (f *goFile) setContent(content []byte) {
 	f.content = content
 
 	// TODO(rstambler): Should we recompute these here?
@@ -235,19 +239,19 @@
 
 	// Remove the package and all of its reverse dependencies from the cache.
 	if f.pkg != nil {
-		v.remove(f.pkg.pkgPath, map[string]struct{}{})
+		f.view.remove(f.pkg.pkgPath, map[string]struct{}{})
 	}
 
 	switch {
 	case f.active && content == nil:
 		// The file was active, so we need to forget its content.
 		f.active = false
-		delete(f.view.config.Overlay, f.filename)
+		delete(f.view.config.Overlay, f.filename())
 		f.content = nil
 	case content != nil:
 		// This is an active overlay, so we update the map.
 		f.active = true
-		f.view.config.Overlay[f.filename] = f.content
+		f.view.config.Overlay[f.filename()] = f.content
 	}
 }
 
@@ -270,14 +274,16 @@
 	// invalidated package.
 	for _, filename := range m.files {
 		if f, _ := v.findFile(span.FileURI(filename)); f != nil {
-			f.pkg = nil
+			if gof, ok := f.(*goFile); ok {
+				gof.pkg = nil
+			}
 		}
 	}
 	delete(v.pcache.packages, pkgPath)
 }
 
 // FindFile returns the file if the given URI is already a part of the view.
-func (v *view) FindFile(ctx context.Context, uri span.URI) *file {
+func (v *view) FindFile(ctx context.Context, uri span.URI) source.File {
 	v.mu.Lock()
 	defer v.mu.Unlock()
 	f, err := v.findFile(uri)
@@ -301,7 +307,7 @@
 }
 
 // getFile is the unlocked internal implementation of GetFile.
-func (v *view) getFile(uri span.URI) (*file, error) {
+func (v *view) getFile(uri span.URI) (viewFile, error) {
 	filename, err := uri.Filename()
 	if err != nil {
 		return nil, err
@@ -314,9 +320,11 @@
 	} else if f != nil {
 		return f, nil
 	}
-	f := &file{
-		view:     v,
-		filename: filename,
+	f := &goFile{
+		fileBase: fileBase{
+			view:  v,
+			fname: filename,
+		},
 	}
 	v.mapFile(uri, f)
 	return f, nil
@@ -340,7 +348,7 @@
 //
 // An error is only returned for an irreparable failure, for example, if the
 // filename in question does not exist.
-func (v *view) findFile(uri span.URI) (*file, error) {
+func (v *view) findFile(uri span.URI) (viewFile, error) {
 	if f := v.filesByURI[uri]; f != nil {
 		// a perfect match
 		return f, nil
@@ -360,7 +368,7 @@
 			return nil, nil // the file may exist, return without an error
 		}
 		for _, c := range candidates {
-			if cStat, err := os.Stat(c.filename); err == nil {
+			if cStat, err := os.Stat(c.filename()); err == nil {
 				if os.SameFile(pathStat, cStat) {
 					// same file, map it
 					v.mapFile(uri, c)
@@ -373,12 +381,16 @@
 	return nil, nil
 }
 
-func (v *view) mapFile(uri span.URI, f *file) {
-	v.filesByURI[uri] = f
+func (f *fileBase) addURI(uri span.URI) int {
 	f.uris = append(f.uris, uri)
-	if f.basename == "" {
-		f.basename = basename(f.filename)
-		v.filesByBase[f.basename] = append(v.filesByBase[f.basename], f)
+	return len(f.uris)
+}
+
+func (v *view) mapFile(uri span.URI, f viewFile) {
+	v.filesByURI[uri] = f
+	if f.addURI(uri) == 1 {
+		basename := basename(f.filename())
+		v.filesByBase[basename] = append(v.filesByBase[basename], f)
 	}
 }
 
diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go
index e320f73..7518e9a 100644
--- a/internal/lsp/code_action.go
+++ b/internal/lsp/code_action.go
@@ -17,7 +17,7 @@
 func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
 	uri := span.NewURI(params.TextDocument.URI)
 	view := s.findView(ctx, uri)
-	_, m, err := newColumnMap(ctx, view, uri)
+	_, m, err := getSourceFile(ctx, view, uri)
 	if err != nil {
 		return nil, err
 	}
@@ -63,7 +63,7 @@
 }
 
 func organizeImports(ctx context.Context, v source.View, s span.Span) ([]protocol.TextEdit, error) {
-	f, m, err := newColumnMap(ctx, v, s.URI())
+	f, m, err := getGoFile(ctx, v, s.URI())
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go
index bcc4e0e..b9d82f4 100644
--- a/internal/lsp/completion.go
+++ b/internal/lsp/completion.go
@@ -18,7 +18,7 @@
 func (s *Server) completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) {
 	uri := span.NewURI(params.TextDocument.URI)
 	view := s.findView(ctx, uri)
-	f, m, err := newColumnMap(ctx, view, uri)
+	f, m, err := getGoFile(ctx, view, uri)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/lsp/definition.go b/internal/lsp/definition.go
index 5a1fe94..b0f996c 100644
--- a/internal/lsp/definition.go
+++ b/internal/lsp/definition.go
@@ -15,7 +15,7 @@
 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)
+	f, m, err := getGoFile(ctx, view, uri)
 	if err != nil {
 		return nil, err
 	}
@@ -35,7 +35,7 @@
 	if err != nil {
 		return nil, err
 	}
-	_, decM, err := newColumnMap(ctx, view, decSpan.URI())
+	_, decM, err := getSourceFile(ctx, view, decSpan.URI())
 	if err != nil {
 		return nil, err
 	}
@@ -49,7 +49,7 @@
 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)
+	f, m, err := getGoFile(ctx, view, uri)
 	if err != nil {
 		return nil, err
 	}
@@ -69,7 +69,7 @@
 	if err != nil {
 		return nil, err
 	}
-	_, identM, err := newColumnMap(ctx, view, identSpan.URI())
+	_, identM, err := getSourceFile(ctx, view, identSpan.URI())
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index 0312d4e..1d0ac83 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -62,7 +62,7 @@
 func toProtocolDiagnostics(ctx context.Context, v source.View, diagnostics []source.Diagnostic) ([]protocol.Diagnostic, error) {
 	reports := []protocol.Diagnostic{}
 	for _, diag := range diagnostics {
-		_, m, err := newColumnMap(ctx, v, diag.Span.URI())
+		_, m, err := getSourceFile(ctx, v, diag.Span.URI())
 		if err != nil {
 			return nil, err
 		}
diff --git a/internal/lsp/format.go b/internal/lsp/format.go
index f591ae1..0c672e8 100644
--- a/internal/lsp/format.go
+++ b/internal/lsp/format.go
@@ -22,7 +22,7 @@
 
 // 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())
+	f, m, err := getGoFile(ctx, v, s.URI())
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/lsp/highlight.go b/internal/lsp/highlight.go
index 288587d..22bb169 100644
--- a/internal/lsp/highlight.go
+++ b/internal/lsp/highlight.go
@@ -15,7 +15,7 @@
 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)
+	f, m, err := getGoFile(ctx, view, uri)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/lsp/hover.go b/internal/lsp/hover.go
index 7d9ae2c..a3d66aa 100644
--- a/internal/lsp/hover.go
+++ b/internal/lsp/hover.go
@@ -16,7 +16,7 @@
 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)
+	f, m, err := getGoFile(ctx, view, uri)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/lsp/link.go b/internal/lsp/link.go
index 33e9188..ecf6e3e 100644
--- a/internal/lsp/link.go
+++ b/internal/lsp/link.go
@@ -15,7 +15,7 @@
 func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) {
 	uri := span.NewURI(params.TextDocument.URI)
 	view := s.findView(ctx, uri)
-	f, m, err := newColumnMap(ctx, view, uri)
+	f, m, err := getGoFile(ctx, view, uri)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index caa9b4b..04eaa81 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -293,7 +293,7 @@
 			}
 			continue
 		}
-		_, m, err := newColumnMap(ctx, r.server.findView(ctx, uri), uri)
+		_, m, err := getSourceFile(ctx, r.server.findView(ctx, uri), uri)
 		if err != nil {
 			t.Error(err)
 		}
diff --git a/internal/lsp/signature_help.go b/internal/lsp/signature_help.go
index 7471321..b28f860 100644
--- a/internal/lsp/signature_help.go
+++ b/internal/lsp/signature_help.go
@@ -15,7 +15,7 @@
 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)
+	f, m, err := getGoFile(ctx, view, uri)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/lsp/source/completion.go b/internal/lsp/source/completion.go
index 2a132d5..d654397 100644
--- a/internal/lsp/source/completion.go
+++ b/internal/lsp/source/completion.go
@@ -200,7 +200,7 @@
 // The prefix is computed based on the preceding identifier and can be used by
 // the client to score the quality of the completion. For instance, some clients
 // may tolerate imperfect matches as valid completion results, since users may make typos.
-func Completion(ctx context.Context, f File, pos token.Pos) ([]CompletionItem, Prefix, error) {
+func Completion(ctx context.Context, f GoFile, pos token.Pos) ([]CompletionItem, Prefix, error) {
 	file := f.GetAST(ctx)
 	pkg := f.GetPackage(ctx)
 	if pkg == nil || pkg.IsIllTyped() {
diff --git a/internal/lsp/source/diagnostics.go b/internal/lsp/source/diagnostics.go
index 032c4af..23b33b6 100644
--- a/internal/lsp/source/diagnostics.go
+++ b/internal/lsp/source/diagnostics.go
@@ -55,7 +55,11 @@
 	if err != nil {
 		return singleDiagnostic(uri, "no file found for %s", uri), nil
 	}
-	pkg := f.GetPackage(ctx)
+	gof, ok := f.(GoFile)
+	if !ok {
+		return singleDiagnostic(uri, "%s is not a go file", uri), nil
+	}
+	pkg := gof.GetPackage(ctx)
 	if pkg == nil {
 		return singleDiagnostic(uri, "%s is not part of a package", uri), nil
 	}
@@ -72,7 +76,7 @@
 		}
 	}
 	// Updates to the diagnostics for this package may need to be propagated.
-	for _, f := range f.GetActiveReverseDeps(ctx) {
+	for _, f := range gof.GetActiveReverseDeps(ctx) {
 		pkg := f.GetPackage(ctx)
 		if pkg == nil {
 			continue
@@ -151,11 +155,16 @@
 
 func pointToSpan(ctx context.Context, v View, spn span.Span) span.Span {
 	// Don't set a range if it's anything other than a type error.
-	diagFile, err := v.GetFile(ctx, spn.URI())
+	f, err := v.GetFile(ctx, spn.URI())
 	if err != nil {
 		v.Logger().Errorf(ctx, "Could find file for diagnostic: %v", spn.URI())
 		return spn
 	}
+	diagFile, ok := f.(GoFile)
+	if !ok {
+		v.Logger().Errorf(ctx, "Not a go file: %v", spn.URI())
+		return spn
+	}
 	tok := diagFile.GetToken(ctx)
 	if tok == nil {
 		v.Logger().Errorf(ctx, "Could not find tokens for diagnostic: %v", spn.URI())
diff --git a/internal/lsp/source/format.go b/internal/lsp/source/format.go
index bcc0d7b..682f075 100644
--- a/internal/lsp/source/format.go
+++ b/internal/lsp/source/format.go
@@ -19,7 +19,7 @@
 )
 
 // Format formats a file with a given range.
-func Format(ctx context.Context, f File, rng span.Range) ([]TextEdit, error) {
+func Format(ctx context.Context, f GoFile, rng span.Range) ([]TextEdit, error) {
 	pkg := f.GetPackage(ctx)
 	if hasParseErrors(pkg.GetErrors()) {
 		return nil, fmt.Errorf("%s has parse errors, not formatting", f.URI())
@@ -52,7 +52,7 @@
 }
 
 // Imports formats a file using the goimports tool.
-func Imports(ctx context.Context, f File, rng span.Range) ([]TextEdit, error) {
+func Imports(ctx context.Context, f GoFile, rng span.Range) ([]TextEdit, error) {
 	formatted, err := imports.Process(f.GetToken(ctx).Name(), f.GetContent(ctx), nil)
 	if err != nil {
 		return nil, err
diff --git a/internal/lsp/source/highlight.go b/internal/lsp/source/highlight.go
index 2952d13..8e81139 100644
--- a/internal/lsp/source/highlight.go
+++ b/internal/lsp/source/highlight.go
@@ -13,7 +13,7 @@
 	"golang.org/x/tools/internal/span"
 )
 
-func Highlight(ctx context.Context, f File, pos token.Pos) []span.Span {
+func Highlight(ctx context.Context, f GoFile, pos token.Pos) []span.Span {
 	fAST := f.GetAST(ctx)
 	fset := f.GetFileSet(ctx)
 	path, _ := astutil.PathEnclosingInterval(fAST, pos, pos)
diff --git a/internal/lsp/source/identifier.go b/internal/lsp/source/identifier.go
index aaf4696..6e43fcc 100644
--- a/internal/lsp/source/identifier.go
+++ b/internal/lsp/source/identifier.go
@@ -20,7 +20,7 @@
 type IdentifierInfo struct {
 	Name  string
 	Range span.Range
-	File  File
+	File  GoFile
 	Type  struct {
 		Range  span.Range
 		Object types.Object
@@ -37,7 +37,7 @@
 
 // Identifier returns identifier information for a position
 // in a file, accounting for a potentially incomplete selector.
-func Identifier(ctx context.Context, v View, f File, pos token.Pos) (*IdentifierInfo, error) {
+func Identifier(ctx context.Context, v View, f GoFile, pos token.Pos) (*IdentifierInfo, error) {
 	if result, err := identifier(ctx, v, f, pos); err != nil || result != nil {
 		return result, err
 	}
@@ -52,7 +52,7 @@
 }
 
 // identifier checks a single position for a potential identifier.
-func identifier(ctx context.Context, v View, f File, pos token.Pos) (*IdentifierInfo, error) {
+func identifier(ctx context.Context, v View, f GoFile, pos token.Pos) (*IdentifierInfo, error) {
 	fAST := f.GetAST(ctx)
 	pkg := f.GetPackage(ctx)
 	if pkg == nil || pkg.IsIllTyped() {
@@ -128,7 +128,7 @@
 	return result, nil
 }
 
-func checkImportSpec(f File, fAST *ast.File, pkg Package, pos token.Pos) (*IdentifierInfo, error) {
+func checkImportSpec(f GoFile, fAST *ast.File, pkg Package, pos token.Pos) (*IdentifierInfo, error) {
 	// Check if pos is in an *ast.ImportSpec.
 	for _, imp := range fAST.Imports {
 		if imp.Pos() <= pos && pos < imp.End() {
@@ -204,10 +204,14 @@
 	if err != nil {
 		return nil, err
 	}
-	declFile, err := v.GetFile(ctx, s.URI())
+	f, err := v.GetFile(ctx, s.URI())
 	if err != nil {
 		return nil, err
 	}
+	declFile, ok := f.(GoFile)
+	if !ok {
+		return nil, fmt.Errorf("not a go file %v", s.URI())
+	}
 	declAST := declFile.GetAST(ctx)
 	path, _ := astutil.PathEnclosingInterval(declAST, rng.Start, rng.End)
 	if path == nil {
diff --git a/internal/lsp/source/signature_help.go b/internal/lsp/source/signature_help.go
index ea5a644..250d428 100644
--- a/internal/lsp/source/signature_help.go
+++ b/internal/lsp/source/signature_help.go
@@ -24,7 +24,7 @@
 	Label string
 }
 
-func SignatureHelp(ctx context.Context, f File, pos token.Pos) (*SignatureInformation, error) {
+func SignatureHelp(ctx context.Context, f GoFile, pos token.Pos) (*SignatureInformation, error) {
 	fAST := f.GetAST(ctx)
 	pkg := f.GetPackage(ctx)
 	if pkg == nil || pkg.IsIllTyped() {
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index 69005c8..b08b110 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -133,9 +133,9 @@
 		if err != nil {
 			t.Fatalf("failed for %v: %v", src, err)
 		}
-		tok := f.GetToken(ctx)
+		tok := f.(source.GoFile).GetToken(ctx)
 		pos := tok.Pos(src.Start().Offset())
-		list, prefix, err := source.Completion(ctx, f, pos)
+		list, prefix, err := source.Completion(ctx, f.(source.GoFile), pos)
 		if err != nil {
 			t.Fatalf("failed for %v: %v", src, err)
 		}
@@ -164,7 +164,7 @@
 			}
 			tok := f.GetToken(ctx)
 			pos := tok.Pos(src.Start().Offset())
-			list, _, err := source.Completion(ctx, f, pos)
+			list, _, err := source.Completion(ctx, f.(source.GoFile), pos)
 			if err != nil {
 				t.Fatalf("failed for %v: %v", src, err)
 			}
@@ -273,7 +273,7 @@
 		if err != nil {
 			t.Fatalf("failed for %v: %v", spn, err)
 		}
-		edits, err := source.Format(ctx, f, rng)
+		edits, err := source.Format(ctx, f.(source.GoFile), rng)
 		if err != nil {
 			if gofmted != "" {
 				t.Error(err)
@@ -297,7 +297,7 @@
 		}
 		tok := f.GetToken(ctx)
 		pos := tok.Pos(d.Src.Start().Offset())
-		ident, err := source.Identifier(ctx, r.view, f, pos)
+		ident, err := source.Identifier(ctx, r.view, f.(source.GoFile), pos)
 		if err != nil {
 			t.Fatalf("failed for %v: %v", d.Src, err)
 		}
@@ -341,7 +341,7 @@
 		}
 		tok := f.GetToken(ctx)
 		pos := tok.Pos(src.Start().Offset())
-		highlights := source.Highlight(ctx, f, pos)
+		highlights := source.Highlight(ctx, f.(source.GoFile), pos)
 		if len(highlights) != len(locations) {
 			t.Fatalf("got %d highlights for %s, expected %d", len(highlights), name, len(locations))
 		}
@@ -360,7 +360,7 @@
 		if err != nil {
 			t.Fatalf("failed for %v: %v", uri, err)
 		}
-		symbols := source.DocumentSymbols(ctx, f)
+		symbols := source.DocumentSymbols(ctx, f.(source.GoFile))
 
 		if len(symbols) != len(expectedSymbols) {
 			t.Errorf("want %d top-level symbols in %v, got %d", len(expectedSymbols), uri, len(symbols))
@@ -424,7 +424,7 @@
 		}
 		tok := f.GetToken(ctx)
 		pos := tok.Pos(spn.Start().Offset())
-		gotSignature, err := source.SignatureHelp(ctx, f, pos)
+		gotSignature, err := source.SignatureHelp(ctx, f.(source.GoFile), pos)
 		if err != nil {
 			t.Fatalf("failed for %v: %v", spn, err)
 		}
diff --git a/internal/lsp/source/symbols.go b/internal/lsp/source/symbols.go
index c0dcaf4..f91f8f4 100644
--- a/internal/lsp/source/symbols.go
+++ b/internal/lsp/source/symbols.go
@@ -40,7 +40,7 @@
 	Children      []Symbol
 }
 
-func DocumentSymbols(ctx context.Context, f File) []Symbol {
+func DocumentSymbols(ctx context.Context, f GoFile) []Symbol {
 	fset := f.GetFileSet(ctx)
 	file := f.GetAST(ctx)
 	pkg := f.GetPackage(ctx)
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 91c82c0..fbbf91f 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -35,22 +35,24 @@
 	Shutdown(ctx context.Context)
 }
 
-// File represents a Go source file that has been type-checked. It is the input
-// to most of the exported functions in this package, as it wraps up the
-// building blocks for most queries. Users of the source package can abstract
-// the loading of packages into their own caching systems.
+// File represents a source file of any type.
 type File interface {
 	URI() span.URI
 	View() View
-	GetAST(ctx context.Context) *ast.File
-	GetFileSet(ctx context.Context) *token.FileSet
-	GetPackage(ctx context.Context) Package
-	GetToken(ctx context.Context) *token.File
 	GetContent(ctx context.Context) []byte
+	GetFileSet(ctx context.Context) *token.FileSet
+	GetToken(ctx context.Context) *token.File
+}
+
+// GoFile represents a Go source file that has been type-checked.
+type GoFile interface {
+	File
+	GetAST(ctx context.Context) *ast.File
+	GetPackage(ctx context.Context) Package
 
 	// GetActiveReverseDeps returns the active files belonging to the reverse
 	// dependencies of this file's package.
-	GetActiveReverseDeps(ctx context.Context) []File
+	GetActiveReverseDeps(ctx context.Context) []GoFile
 }
 
 // Package represents a Go package that has been type-checked. It maintains
diff --git a/internal/lsp/symbols.go b/internal/lsp/symbols.go
index da57ec2..2ee526a 100644
--- a/internal/lsp/symbols.go
+++ b/internal/lsp/symbols.go
@@ -15,7 +15,7 @@
 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)
+	f, m, err := getGoFile(ctx, view, uri)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go
index 1667773..5402f23 100644
--- a/internal/lsp/text_synchronization.go
+++ b/internal/lsp/text_synchronization.go
@@ -65,7 +65,7 @@
 
 	uri := span.NewURI(params.TextDocument.URI)
 	view := s.findView(ctx, uri)
-	file, m, err := newColumnMap(ctx, view, uri)
+	file, m, err := getSourceFile(ctx, view, uri)
 	if err != nil {
 		return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found")
 	}
diff --git a/internal/lsp/util.go b/internal/lsp/util.go
index 244e2cb..0d61de5 100644
--- a/internal/lsp/util.go
+++ b/internal/lsp/util.go
@@ -46,7 +46,7 @@
 	}
 }
 
-func newColumnMap(ctx context.Context, v source.View, uri span.URI) (source.File, *protocol.ColumnMapper, error) {
+func getSourceFile(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
@@ -58,3 +58,15 @@
 	m := protocol.NewColumnMapper(f.URI(), f.GetFileSet(ctx), tok, f.GetContent(ctx))
 	return f, m, nil
 }
+
+func getGoFile(ctx context.Context, v source.View, uri span.URI) (source.GoFile, *protocol.ColumnMapper, error) {
+	f, m, err := getSourceFile(ctx, v, uri)
+	if err != nil {
+		return nil, nil, err
+	}
+	gof, ok := f.(source.GoFile)
+	if !ok {
+		return nil, nil, fmt.Errorf("not a go file %v", f.URI())
+	}
+	return gof, m, nil
+}