diff --git a/gopls/internal/regtest/misc/template_test.go b/gopls/internal/regtest/misc/template_test.go deleted file mode 100644 index 6d1419a1b3..0000000000 --- a/gopls/internal/regtest/misc/template_test.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2021 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 misc - -import ( - "strings" - "testing" - - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" -) - -const filesA = ` --- go.mod -- -module mod.com - -go 1.12 --- b.gotmpl -- -{{define "A"}}goo{{end}} --- a.tmpl -- -{{template "A"}} -` - -func TestSuffixes(t *testing.T) { - WithOptions( - EditorConfig{ - AllExperiments: true, - Settings: map[string]interface{}{ - "templateExtensions": []string{"tmpl", "gotmpl"}, - }, - }, - ).Run(t, filesA, func(t *testing.T, env *Env) { - env.OpenFile("a.tmpl") - x := env.RegexpSearch("a.tmpl", `A`) - file, pos := env.GoToDefinition("a.tmpl", x) - refs := env.References(file, pos) - if len(refs) != 2 { - t.Fatalf("got %v reference(s), want 2", len(refs)) - } - // make sure we got one from b.gotmpl - want := env.Sandbox.Workdir.URI("b.gotmpl") - if refs[0].URI != want && refs[1].URI != want { - t.Errorf("failed to find reference to %s", shorten(want)) - for i, r := range refs { - t.Logf("%d: URI:%s %v", i, shorten(r.URI), r.Range) - } - } - - content, npos := env.Hover(file, pos) - if pos != npos { - t.Errorf("pos? got %v, wanted %v", npos, pos) - } - if content.Value != "template A defined" { - t.Errorf("got %s, wanted 'template A defined", content.Value) - } - }) -} - -// shorten long URIs -func shorten(fn protocol.DocumentURI) string { - if len(fn) <= 20 { - return string(fn) - } - pieces := strings.Split(string(fn), "/") - if len(pieces) < 2 { - return string(fn) - } - j := len(pieces) - return pieces[j-2] + "/" + pieces[j-1] -} - -// Hover, SemTok, Diagnose with errors -// and better coverage diff --git a/gopls/internal/regtest/template/template_test.go b/gopls/internal/regtest/template/template_test.go new file mode 100644 index 0000000000..6f4ec0185c --- /dev/null +++ b/gopls/internal/regtest/template/template_test.go @@ -0,0 +1,150 @@ +// Copyright 2022 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 template + +import ( + "strings" + "testing" + + "golang.org/x/tools/gopls/internal/hooks" + "golang.org/x/tools/internal/lsp/protocol" + . "golang.org/x/tools/internal/lsp/regtest" +) + +func TestMain(m *testing.M) { + Main(m, hooks.Options) +} + +func TestTemplatesFromExtensions(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.12 +-- hello.tmpl -- +{{range .Planets}} +Hello {{}} <-- missing body +{{end}} +` + + WithOptions( + EditorConfig{ + Settings: map[string]interface{}{ + "templateExtensions": []string{"tmpl"}, + }, + }, + ).Run(t, files, func(t *testing.T, env *Env) { + // TODO: can we move this diagnostic onto {{}}? + env.Await(env.DiagnosticAtRegexp("hello.tmpl", "()Hello {{}}")) + env.WriteWorkspaceFile("hello.tmpl", "{{range .Planets}}\nHello {{.}}\n{{end}}") + env.Await(EmptyDiagnostics("hello.tmpl")) + }) +} + +func TestTemplatesFromLangID(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.12 +` + + Run(t, files, func(t *testing.T, env *Env) { + env.CreateBuffer("hello.tmpl", "") + env.Await( + OnceMet( + env.DoneWithOpen(), + NoDiagnostics("hello.tmpl"), // Don't get spurious errors for empty templates. + ), + ) + env.SetBufferContent("hello.tmpl", "{{range .Planets}}\nHello {{}}\n{{end}}") + env.Await(env.DiagnosticAtRegexp("hello.tmpl", "()Hello {{}}")) + env.RegexpReplace("hello.tmpl", "{{}}", "{{.}}") + env.Await(EmptyOrNoDiagnostics("hello.tmpl")) + }) +} + +func TestClosingTemplatesMakesDiagnosticsDisappear(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.12 +-- hello.tmpl -- +{{range .Planets}} +Hello {{}} <-- missing body +{{end}} +` + + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("hello.tmpl") + env.Await(env.DiagnosticAtRegexp("hello.tmpl", "()Hello {{}}")) + // Since we don't have templateExtensions configured, closing hello.tmpl + // should make its diagnostics disappear. + env.CloseBuffer("hello.tmpl") + env.Await(EmptyDiagnostics("hello.tmpl")) + }) +} + +func TestMultipleSuffixes(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.12 +-- b.gotmpl -- +{{define "A"}}goo{{end}} +-- a.tmpl -- +{{template "A"}} +` + + WithOptions( + EditorConfig{ + Settings: map[string]interface{}{ + "templateExtensions": []string{"tmpl", "gotmpl"}, + }, + }, + ).Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("a.tmpl") + x := env.RegexpSearch("a.tmpl", `A`) + file, pos := env.GoToDefinition("a.tmpl", x) + refs := env.References(file, pos) + if len(refs) != 2 { + t.Fatalf("got %v reference(s), want 2", len(refs)) + } + // make sure we got one from b.gotmpl + want := env.Sandbox.Workdir.URI("b.gotmpl") + if refs[0].URI != want && refs[1].URI != want { + t.Errorf("failed to find reference to %s", shorten(want)) + for i, r := range refs { + t.Logf("%d: URI:%s %v", i, shorten(r.URI), r.Range) + } + } + + content, npos := env.Hover(file, pos) + if pos != npos { + t.Errorf("pos? got %v, wanted %v", npos, pos) + } + if content.Value != "template A defined" { + t.Errorf("got %s, wanted 'template A defined", content.Value) + } + }) +} + +// shorten long URIs +func shorten(fn protocol.DocumentURI) string { + if len(fn) <= 20 { + return string(fn) + } + pieces := strings.Split(string(fn), "/") + if len(pieces) < 2 { + return string(fn) + } + j := len(pieces) + return pieces[j-2] + "/" + pieces[j-1] +} + +// Hover, SemTok, Diagnose with errors +// and better coverage diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go index fe7d4b5c1b..e47a1bf53d 100644 --- a/internal/lsp/fake/editor.go +++ b/internal/lsp/fake/editor.go @@ -9,6 +9,7 @@ import ( "context" "fmt" "os" + "path" "path/filepath" "regexp" "strings" @@ -114,6 +115,14 @@ type EditorConfig struct { // Whether to edit files with windows line endings. WindowsLineEndings bool + // Map of language ID -> regexp to match, used to set the file type of new + // buffers. Applied as an overlay on top of the following defaults: + // "go" -> ".*\.go" + // "go.mod" -> "go\.mod" + // "go.sum" -> "go\.sum" + // "gotmpl" -> ".*tmpl" + FileAssociations map[string]string + // Settings holds arbitrary additional settings to apply to the gopls config. // TODO(rfindley): replace existing EditorConfig fields with Settings. Settings map[string]interface{} @@ -378,21 +387,6 @@ func (e *Editor) OpenFile(ctx context.Context, path string) error { return e.createBuffer(ctx, path, false, content) } -func textDocumentItem(wd *Workdir, buf buffer) protocol.TextDocumentItem { - uri := wd.URI(buf.path) - languageID := "" - if strings.HasSuffix(buf.path, ".go") { - // TODO: what about go.mod files? What is their language ID? - languageID = "go" - } - return protocol.TextDocumentItem{ - URI: uri, - LanguageID: languageID, - Version: int32(buf.version), - Text: buf.text(), - } -} - // CreateBuffer creates a new unsaved buffer corresponding to the workdir path, // containing the given textual content. func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error { @@ -410,7 +404,13 @@ func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, cont e.mu.Lock() defer e.mu.Unlock() e.buffers[path] = buf - item := textDocumentItem(e.sandbox.Workdir, buf) + + item := protocol.TextDocumentItem{ + URI: e.sandbox.Workdir.URI(buf.path), + LanguageID: e.languageID(buf.path), + Version: int32(buf.version), + Text: buf.text(), + } if e.Server != nil { if err := e.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{ @@ -425,6 +425,29 @@ func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, cont return nil } +var defaultFileAssociations = map[string]*regexp.Regexp{ + "go": regexp.MustCompile(`^.*\.go$`), // '$' is important: don't match .gotmpl! + "go.mod": regexp.MustCompile(`^go\.mod$`), + "go.sum": regexp.MustCompile(`^go\.sum$`), + "gotmpl": regexp.MustCompile(`^.*tmpl$`), +} + +func (e *Editor) languageID(p string) string { + base := path.Base(p) + for lang, re := range e.Config.FileAssociations { + re := regexp.MustCompile(re) + if re.MatchString(base) { + return lang + } + } + for lang, re := range defaultFileAssociations { + if re.MatchString(base) { + return lang + } + } + return "" +} + // lines returns line-ending agnostic line representation of content. func lines(content string) []string { lines := strings.Split(content, "\n")