internal/template: identify template files by the templateExtensions option

Make the language id (sent from the client) 'gotmpl' equivalent to 'tmpl'

Wherever a view is known, use its options to determine which files
are template files. Whenever the client sends an explicit
languageID, use that.

Partially fixes golang/vscode-go#1957

Change-Id: I04cd630d6c6c80e0a78c2fafb6ddc1166ce86829
Reviewed-on: https://go-review.googlesource.com/c/tools/+/376854
Trust: Peter Weinberger <pjw@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go
index be03e63..ac670b5 100644
--- a/internal/lsp/cache/cache.go
+++ b/internal/lsp/cache/cache.go
@@ -168,10 +168,6 @@
 	return h.uri
 }
 
-func (h *fileHandle) Kind() source.FileKind {
-	return source.DetectLanguage("", h.uri.Filename())
-}
-
 func (h *fileHandle) Hash() string {
 	return h.hash
 }
@@ -180,7 +176,6 @@
 	return source.FileIdentity{
 		URI:  h.uri,
 		Hash: h.hash,
-		Kind: h.Kind(),
 	}
 }
 
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index c5b5a3d..84e8245 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -57,7 +57,7 @@
 			uri := span.URI(scope)
 			// Don't try to load a file that doesn't exist.
 			fh := s.FindFile(uri)
-			if fh == nil || fh.Kind() != source.Go {
+			if fh == nil || s.View().FileKind(uri) != source.Go {
 				continue
 			}
 			query = append(query, fmt.Sprintf("file=%s", uri.Filename()))
@@ -264,7 +264,7 @@
 	for _, fh := range files {
 		// Place the diagnostics on the package or module declarations.
 		var rng protocol.Range
-		switch fh.Kind() {
+		switch s.view.FileKind(fh.URI()) {
 		case source.Go:
 			if pgf, err := s.ParseGo(ctx, fh, source.ParseHeader); err == nil {
 				pkgDecl := span.NewRange(s.FileSet(), pgf.File.Package, pgf.File.Name.End())
diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go
index a915d05..f881a02 100644
--- a/internal/lsp/cache/mod.go
+++ b/internal/lsp/cache/mod.go
@@ -164,7 +164,7 @@
 }
 
 func (s *snapshot) ModWhy(ctx context.Context, fh source.FileHandle) (map[string]string, error) {
-	if fh.Kind() != source.Mod {
+	if s.View().FileKind(fh.URI()) != source.Mod {
 		return nil, fmt.Errorf("%s is not a go.mod file", fh.URI())
 	}
 	if handle := s.getModWhyHandle(fh.URI()); handle != nil {
diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go
index 0de0f70..e761373 100644
--- a/internal/lsp/cache/parse.go
+++ b/internal/lsp/cache/parse.go
@@ -13,6 +13,7 @@
 	"go/scanner"
 	"go/token"
 	"go/types"
+	"path/filepath"
 	"reflect"
 	"strconv"
 	"strings"
@@ -246,7 +247,8 @@
 	ctx, done := event.Start(ctx, "cache.parseGo", tag.File.Of(fh.URI().Filename()))
 	defer done()
 
-	if fh.Kind() != source.Go {
+	ext := filepath.Ext(fh.URI().Filename())
+	if ext != ".go" && ext != "" { // files generated by cgo have no extension
 		return &parseGoData{err: errors.Errorf("cannot parse non-Go file %s", fh.URI())}
 	}
 	src, err := fh.Read()
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index a65b8fe..e906ecc 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -62,7 +62,6 @@
 	return source.FileIdentity{
 		URI:  o.uri,
 		Hash: o.hash,
-		Kind: o.kind,
 	}
 }
 
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index 53f97f4..1b4ef46 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -159,7 +159,8 @@
 }
 
 func (s *snapshot) Templates() map[span.URI]source.VersionedFileHandle {
-	if len(s.view.Options().TemplateExtensions) == 0 {
+	opts := s.view.Options().TemplateExtensions
+	if len(opts) == 0 {
 		return nil
 	}
 
@@ -168,8 +169,18 @@
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
+	isin := func(s string, a []string) bool {
+		for _, x := range a {
+			if x == s || "."+x == s {
+				return true
+			}
+		}
+		return false
+	}
+
 	for k, x := range s.files {
-		if strings.HasSuffix(filepath.Ext(k.Filename()), "tmpl") {
+		suffix := filepath.Ext(k.Filename())
+		if isin(suffix, opts) {
 			ans[k] = x
 		}
 	}
@@ -516,8 +527,8 @@
 	if err != nil {
 		return nil, err
 	}
-	if fh.Kind() != source.Go {
-		return nil, fmt.Errorf("no packages for non-Go file %s", uri)
+	if kind := s.view.FileKind(fh.FileIdentity().URI); kind != source.Go {
+		return nil, fmt.Errorf("no packages for non-Go file %s (%v)", uri, kind)
 	}
 	knownIDs, err := s.getOrLoadIDsForURI(ctx, uri)
 	if err != nil {
@@ -780,7 +791,7 @@
 	return s.workspacePackages[id]
 }
 
-const fileExtensions = "go,mod,sum,work,tmpl"
+const fileExtensions = "go,mod,sum,work"
 
 func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]struct{} {
 	extensions := fileExtensions
@@ -1574,7 +1585,7 @@
 	var files []source.VersionedFileHandle
 	for uri, fh := range s.files {
 		// Don't try to reload metadata for go.mod files.
-		if fh.Kind() != source.Go {
+		if s.view.FileKind(uri) != source.Go {
 			continue
 		}
 		// If the URI doesn't belong to this view, then it's not in a workspace
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index fcff02a..b4fe38e 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -241,6 +241,26 @@
 	return v.options
 }
 
+func (v *View) FileKind(URI span.URI) source.FileKind {
+	got := filepath.Ext(URI.Filename())
+	switch got {
+	case ".go":
+		return source.Go
+	case ".mod":
+		return source.Mod
+	case ".sum":
+		return source.Sum
+	}
+	exts := v.Options().TemplateExtensions
+	for _, ext := range exts {
+		if got == ext || got == "."+ext {
+			return source.Tmpl
+		}
+	}
+	// and now what? This should never happen, but it does for cgo before go1.15
+	return source.Go
+}
+
 func minorOptionsChange(a, b *source.Options) bool {
 	// Check if any of the settings that modify our understanding of files have been changed
 	if !reflect.DeepEqual(a.Env, b.Env) {
diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go
index 8245e12..06a33a1 100644
--- a/internal/lsp/cmd/cmd.go
+++ b/internal/lsp/cmd/cmd.go
@@ -497,7 +497,7 @@
 	p := &protocol.DidOpenTextDocumentParams{
 		TextDocument: protocol.TextDocumentItem{
 			URI:        protocol.URIFromSpanURI(uri),
-			LanguageID: source.DetectLanguage("", file.uri.Filename()).String(),
+			LanguageID: "go",
 			Version:    1,
 			Text:       string(file.mapper.Content),
 		},
diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go
index 526d279..45d2d6e 100644
--- a/internal/lsp/code_action.go
+++ b/internal/lsp/code_action.go
@@ -30,9 +30,10 @@
 	uri := fh.URI()
 
 	// Determine the supported actions for this file kind.
-	supportedCodeActions, ok := snapshot.View().Options().SupportedCodeActions[fh.Kind()]
+	kind := snapshot.View().FileKind(uri)
+	supportedCodeActions, ok := snapshot.View().Options().SupportedCodeActions[kind]
 	if !ok {
-		return nil, fmt.Errorf("no supported code actions for %v file kind", fh.Kind())
+		return nil, fmt.Errorf("no supported code actions for %v file kind", kind)
 	}
 
 	// The Only field of the context specifies which code actions the client wants.
@@ -67,7 +68,7 @@
 	}
 
 	var codeActions []protocol.CodeAction
-	switch fh.Kind() {
+	switch kind {
 	case source.Mod:
 		if diagnostics := params.Context.Diagnostics; len(diagnostics) > 0 {
 			diags, err := mod.DiagnosticsForMod(ctx, snapshot, fh)
diff --git a/internal/lsp/code_lens.go b/internal/lsp/code_lens.go
index 6e371fc..345a21d 100644
--- a/internal/lsp/code_lens.go
+++ b/internal/lsp/code_lens.go
@@ -23,7 +23,7 @@
 		return nil, err
 	}
 	var lenses map[command.Command]source.LensFunc
-	switch fh.Kind() {
+	switch snapshot.View().FileKind(fh.URI()) {
 	case source.Mod:
 		lenses = mod.LensFuncs()
 	case source.Go:
diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go
index 4523d34..4e90405 100644
--- a/internal/lsp/completion.go
+++ b/internal/lsp/completion.go
@@ -27,7 +27,7 @@
 	}
 	var candidates []completion.CompletionItem
 	var surrounding *completion.Selection
-	switch fh.Kind() {
+	switch snapshot.View().FileKind(fh.URI()) {
 	case source.Go:
 		candidates, surrounding, err = completion.Completion(ctx, snapshot, fh, params.Position, params.Context)
 	case source.Mod:
diff --git a/internal/lsp/definition.go b/internal/lsp/definition.go
index ab4aaed..b55f26d 100644
--- a/internal/lsp/definition.go
+++ b/internal/lsp/definition.go
@@ -18,7 +18,7 @@
 	if !ok {
 		return nil, err
 	}
-	if fh.Kind() == source.Tmpl {
+	if snapshot.View().FileKind(fh.URI()) == source.Tmpl {
 		return template.Definition(snapshot, fh, params.Position)
 	}
 	ident, err := source.Identifier(ctx, snapshot, fh, params.Position)
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index f9e1a47..64379af 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -416,7 +416,7 @@
 // If they cannot and the workspace is not otherwise unloaded, it also surfaces
 // a warning, suggesting that the user check the file for build tags.
 func (s *Server) checkForOrphanedFile(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle) *source.Diagnostic {
-	if fh.Kind() != source.Go {
+	if snapshot.View().FileKind(fh.URI()) != source.Go {
 		return nil
 	}
 	// builtin files won't have a package, but they are never orphaned.
diff --git a/internal/lsp/format.go b/internal/lsp/format.go
index 62b25d8..467bcce 100644
--- a/internal/lsp/format.go
+++ b/internal/lsp/format.go
@@ -18,7 +18,7 @@
 	if !ok {
 		return nil, err
 	}
-	switch fh.Kind() {
+	switch snapshot.View().FileKind(fh.URI()) {
 	case source.Mod:
 		return mod.Format(ctx, snapshot, fh)
 	case source.Go:
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
index a946c80..7f23893 100644
--- a/internal/lsp/general.go
+++ b/internal/lsp/general.go
@@ -466,7 +466,8 @@
 		release()
 		return nil, nil, false, func() {}, err
 	}
-	if expectKind != source.UnknownKind && fh.Kind() != expectKind {
+	kind := snapshot.View().FileKind(fh.URI())
+	if expectKind != source.UnknownKind && kind != expectKind {
 		// Wrong kind of file. Nothing to do.
 		release()
 		return nil, nil, false, func() {}, nil
diff --git a/internal/lsp/highlight.go b/internal/lsp/highlight.go
index a350dd5..71bd7d9 100644
--- a/internal/lsp/highlight.go
+++ b/internal/lsp/highlight.go
@@ -21,7 +21,7 @@
 		return nil, err
 	}
 
-	if fh.Kind() == source.Tmpl {
+	if snapshot.View().FileKind(fh.URI()) == source.Tmpl {
 		return template.Highlight(ctx, snapshot, fh, params.Position)
 	}
 
diff --git a/internal/lsp/hover.go b/internal/lsp/hover.go
index 1e118bc..9125c23 100644
--- a/internal/lsp/hover.go
+++ b/internal/lsp/hover.go
@@ -19,7 +19,7 @@
 	if !ok {
 		return nil, err
 	}
-	switch fh.Kind() {
+	switch snapshot.View().FileKind(fh.URI()) {
 	case source.Mod:
 		return mod.Hover(ctx, snapshot, fh, params.Position)
 	case source.Go:
diff --git a/internal/lsp/link.go b/internal/lsp/link.go
index 87692fa..d76f1d0 100644
--- a/internal/lsp/link.go
+++ b/internal/lsp/link.go
@@ -30,7 +30,7 @@
 	if !ok {
 		return nil, err
 	}
-	switch fh.Kind() {
+	switch snapshot.View().FileKind(fh.URI()) {
 	case source.Mod:
 		links, err = modLinks(ctx, snapshot, fh)
 	case source.Go:
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index 8a66730..ccfe2c9 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -71,8 +71,7 @@
 
 	var modifications []source.FileModification
 	for filename, content := range datum.Config.Overlay {
-		kind := source.DetectLanguage("", filename)
-		if kind != source.Go {
+		if filepath.Ext(filename) != ".go" {
 			continue
 		}
 		modifications = append(modifications, source.FileModification{
@@ -187,7 +186,7 @@
 }
 
 func (r *runner) CodeLens(t *testing.T, uri span.URI, want []protocol.CodeLens) {
-	if source.DetectLanguage("", uri.Filename()) != source.Mod {
+	if !strings.HasSuffix(uri.Filename(), "go.mod") {
 		return
 	}
 	got, err := r.server.codeLens(r.ctx, &protocol.CodeLensParams{
diff --git a/internal/lsp/references.go b/internal/lsp/references.go
index d8f2f1e..b5bb00f 100644
--- a/internal/lsp/references.go
+++ b/internal/lsp/references.go
@@ -18,7 +18,7 @@
 	if !ok {
 		return nil, err
 	}
-	if fh.Kind() == source.Tmpl {
+	if snapshot.View().FileKind(fh.URI()) == source.Tmpl {
 		return template.References(ctx, snapshot, fh, params)
 	}
 	references, err := source.References(ctx, snapshot, fh, params.Position, params.Context.IncludeDeclaration)
diff --git a/internal/lsp/semantic.go b/internal/lsp/semantic.go
index c3ea15c..3925ce3 100644
--- a/internal/lsp/semantic.go
+++ b/internal/lsp/semantic.go
@@ -70,7 +70,8 @@
 		// the client won't remember the wrong answer
 		return nil, errors.Errorf("semantictokens are disabled")
 	}
-	if fh.Kind() == source.Tmpl {
+	kind := snapshot.View().FileKind(fh.URI())
+	if kind == source.Tmpl {
 		// this is a little cumbersome to avoid both exporting 'encoded' and its methods
 		// and to avoid import cycles
 		e := &encoded{
@@ -87,7 +88,7 @@
 		}
 		return template.SemanticTokens(ctx, snapshot, fh.URI(), add, data)
 	}
-	if fh.Kind() != source.Go {
+	if kind != source.Go {
 		return nil, nil
 	}
 	pkg, err := snapshot.PackageForFile(ctx, fh.URI(), source.TypecheckFull, source.WidestPackage)
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index d0e6b8a..4a2c43f 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -66,8 +66,7 @@
 
 	var modifications []source.FileModification
 	for filename, content := range datum.Config.Overlay {
-		kind := source.DetectLanguage("", filename)
-		if kind != source.Go {
+		if filepath.Ext(filename) != ".go" {
 			continue
 		}
 		modifications = append(modifications, source.FileModification{
diff --git a/internal/lsp/source/util.go b/internal/lsp/source/util.go
index 9500eee..c9b4878 100644
--- a/internal/lsp/source/util.go
+++ b/internal/lsp/source/util.go
@@ -162,34 +162,39 @@
 var generatedRx = regexp.MustCompile(`// .*DO NOT EDIT\.?`)
 
 func DetectLanguage(langID, filename string) FileKind {
-	switch langID {
-	case "go":
-		return Go
-	case "go.mod":
-		return Mod
-	case "go.sum":
-		return Sum
-	case "tmpl":
-		return Tmpl
+	// use the langID if the client sent it
+	if langID != "" {
+		switch langID {
+		case "go":
+			return Go
+		case "go.mod":
+			return Mod
+		case "go.sum":
+			return Sum
+		case "tmpl", "gotmpl":
+			return Tmpl
+		default:
+			return UnknownKind
+		}
 	}
-	// Fallback to detecting the language based on the file extension.
+	// Detect the language based on the file extension.
 	switch ext := filepath.Ext(filename); ext {
 	case ".mod":
 		return Mod
 	case ".sum":
 		return Sum
+	case ".go":
+		return Go
 	default:
-		if strings.HasSuffix(ext, "tmpl") {
-			// .tmpl, .gotmpl, etc
-			return Tmpl
-		}
-		// It's a Go file, or we shouldn't be seeing it
+		// (for instance, before go1.15 cgo files had no extension)
 		return Go
 	}
 }
 
 func (k FileKind) String() string {
 	switch k {
+	case Go:
+		return "go"
 	case Mod:
 		return "go.mod"
 	case Sum:
@@ -197,7 +202,7 @@
 	case Tmpl:
 		return "tmpl"
 	default:
-		return "go"
+		return fmt.Sprintf("unk%d", k)
 	}
 }
 
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 2285e5a..5064856 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -266,6 +266,9 @@
 
 	// RegisterModuleUpgrades registers that upgrades exist for the given modules.
 	RegisterModuleUpgrades(upgrades map[string]string)
+
+	// FileKind returns the type of a file
+	FileKind(uri span.URI) FileKind
 }
 
 // A FileSource maps uris to FileHandles. This abstraction exists both for
@@ -498,7 +501,6 @@
 // FileHandle represents a handle to a specific version of a single file.
 type FileHandle interface {
 	URI() span.URI
-	Kind() FileKind
 
 	// FileIdentity returns a FileIdentity for the file, even if there was an
 	// error reading it.
@@ -516,17 +518,14 @@
 
 	// Identifier represents a unique identifier for the file's content.
 	Hash string
-
-	// Kind is the file's kind.
-	Kind FileKind
 }
 
 func (id FileIdentity) String() string {
-	return fmt.Sprintf("%s%s%s", id.URI, id.Hash, id.Kind)
+	return fmt.Sprintf("%s%s", id.URI, id.Hash)
 }
 
 // FileKind describes the kind of the file in question.
-// It can be one of Go, mod, or sum.
+// It can be one of Go,mod, Sum, or Tmpl.
 type FileKind int
 
 const (
diff --git a/internal/lsp/symbols.go b/internal/lsp/symbols.go
index 5bde1bd..49566e0 100644
--- a/internal/lsp/symbols.go
+++ b/internal/lsp/symbols.go
@@ -24,7 +24,7 @@
 		return []interface{}{}, err
 	}
 	var docSymbols []protocol.DocumentSymbol
-	if fh.Kind() == source.Tmpl {
+	if snapshot.View().FileKind(fh.URI()) == source.Tmpl {
 		docSymbols, err = template.DocumentSymbols(snapshot, fh)
 	} else {
 		docSymbols, err = source.DocumentSymbols(ctx, snapshot, fh)
diff --git a/internal/lsp/workspace.go b/internal/lsp/workspace.go
index 02feae5..c239942 100644
--- a/internal/lsp/workspace.go
+++ b/internal/lsp/workspace.go
@@ -79,8 +79,8 @@
 		if err != nil {
 			return err
 		}
-		snapshot, release := view.Snapshot(ctx)
 		go func() {
+			snapshot, release := view.Snapshot(ctx)
 			defer release()
 			s.diagnoseDetached(snapshot)
 		}()