diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go index b9bae20cfe..2ccf62f4ac 100644 --- a/internal/lsp/cache/check.go +++ b/internal/lsp/cache/check.go @@ -258,7 +258,7 @@ func (imp *importer) typeCheck(ctx context.Context, cph *checkPackageHandle, m * go func(i int, ph source.ParseGoHandle) { defer wg.Done() - files[i], parseErrors[i] = ph.Parse(ctx) + files[i], _, parseErrors[i] = ph.Parse(ctx) }(i, ph) } wg.Wait() @@ -350,7 +350,7 @@ func (imp *importer) cachePerFile(ctx context.Context, gof *goFile, ph source.Pa } gof.pkgs[cph.m.id] = cph - file, err := ph.Parse(ctx) + file, _, err := ph.Parse(ctx) if file == nil { return errors.Errorf("no AST for %s: %v", ph.File().Identity().URI, err) } diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go index 1129c75759..14b58791b0 100644 --- a/internal/lsp/cache/load.go +++ b/internal/lsp/cache/load.go @@ -190,7 +190,7 @@ func (v *view) shouldRunGopackages(ctx context.Context, f *goFile, fh source.Fil return true } // Get file content in case we don't already have it. - parsed, err := v.session.cache.ParseGoHandle(fh, source.ParseHeader).Parse(ctx) + parsed, _, err := v.session.cache.ParseGoHandle(fh, source.ParseHeader).Parse(ctx) if err == context.Canceled { log.Error(ctx, "parsing file header", err, tag.Of("file", f.URI())) return false diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go index 328847c571..2dda5b6c4d 100644 --- a/internal/lsp/cache/parse.go +++ b/internal/lsp/cache/parse.go @@ -13,9 +13,11 @@ import ( "go/token" "reflect" + "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/telemetry" "golang.org/x/tools/internal/memoize" + "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/telemetry/log" "golang.org/x/tools/internal/telemetry/trace" errors "golang.org/x/xerrors" @@ -39,8 +41,9 @@ type parseGoHandle struct { type parseGoData struct { memoize.NoCopy - ast *ast.File - err error + ast *ast.File + mapper *protocol.ColumnMapper + err error } func (c *cache) ParseGoHandle(fh source.FileHandle, mode source.ParseMode) source.ParseGoHandle { @@ -51,6 +54,17 @@ func (c *cache) ParseGoHandle(fh source.FileHandle, mode source.ParseMode) sourc h := c.store.Bind(key, func(ctx context.Context) interface{} { data := &parseGoData{} data.ast, data.err = parseGo(ctx, c, fh, mode) + tok := c.FileSet().File(data.ast.Pos()) + if tok == nil { + return data + } + uri := fh.Identity().URI + content, _, err := fh.Read(ctx) + if err != nil { + data.err = err + return data + } + data.mapper = newColumnMapper(uri, c.FileSet(), tok, content) return data }) return &parseGoHandle{ @@ -59,6 +73,13 @@ func (c *cache) ParseGoHandle(fh source.FileHandle, mode source.ParseMode) sourc mode: mode, } } +func newColumnMapper(uri span.URI, fset *token.FileSet, tok *token.File, content []byte) *protocol.ColumnMapper { + return &protocol.ColumnMapper{ + URI: uri, + Converter: span.NewTokenConverter(fset, tok), + Content: content, + } +} func (h *parseGoHandle) File() source.FileHandle { return h.file @@ -68,22 +89,22 @@ func (h *parseGoHandle) Mode() source.ParseMode { return h.mode } -func (h *parseGoHandle) Parse(ctx context.Context) (*ast.File, error) { +func (h *parseGoHandle) Parse(ctx context.Context) (*ast.File, *protocol.ColumnMapper, error) { v := h.handle.Get(ctx) if v == nil { - return nil, ctx.Err() + return nil, nil, ctx.Err() } data := v.(*parseGoData) - return data.ast, data.err + return data.ast, data.mapper, data.err } -func (h *parseGoHandle) Cached(ctx context.Context) (*ast.File, error) { +func (h *parseGoHandle) Cached(ctx context.Context) (*ast.File, *protocol.ColumnMapper, error) { v := h.handle.Cached() if v == nil { - return nil, errors.Errorf("no cached value for %s", h.file.Identity().URI) + return nil, nil, errors.Errorf("no cached value for %s", h.file.Identity().URI) } data := v.(*parseGoData) - return data.ast, data.err + return data.ast, data.mapper, data.err } func hashParseKey(ph source.ParseGoHandle) string { diff --git a/internal/lsp/cache/pkg.go b/internal/lsp/cache/pkg.go index 69c9a74175..757dc15f7e 100644 --- a/internal/lsp/cache/pkg.go +++ b/internal/lsp/cache/pkg.go @@ -7,7 +7,6 @@ package cache import ( "context" "go/ast" - "go/token" "go/types" "sort" "sync" @@ -155,7 +154,7 @@ func (pkg *pkg) GetHandles() []source.ParseGoHandle { func (pkg *pkg) GetSyntax(ctx context.Context) []*ast.File { var syntax []*ast.File for _, ph := range pkg.files { - file, _ := ph.Cached(ctx) + file, _, _ := ph.Cached(ctx) if file != nil { syntax = append(syntax, file) } @@ -203,7 +202,7 @@ func (pkg *pkg) GetDiagnostics() []source.Diagnostic { return diags } -func (p *pkg) FindFile(ctx context.Context, uri span.URI, pos token.Pos) (source.ParseGoHandle, *ast.File, source.Package, error) { +func (p *pkg) FindFile(ctx context.Context, uri span.URI) (source.ParseGoHandle, *ast.File, source.Package, error) { queue := []*pkg{p} seen := make(map[string]bool) @@ -214,13 +213,11 @@ func (p *pkg) FindFile(ctx context.Context, uri span.URI, pos token.Pos) (source for _, ph := range pkg.files { if ph.File().Identity().URI == uri { - file, err := ph.Cached(ctx) + file, _, err := ph.Cached(ctx) if file == nil { return nil, nil, nil, err } - if file.Pos() <= pos && pos <= file.End() { - return ph, file, pkg, nil - } + return ph, file, pkg, nil } } for _, dep := range pkg.imports { diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go index f5696e0dae..a765fabc07 100644 --- a/internal/lsp/cache/view.go +++ b/internal/lsp/cache/view.go @@ -8,7 +8,6 @@ import ( "context" "fmt" "go/ast" - "go/parser" "go/token" "go/types" "os" @@ -310,8 +309,11 @@ func (v *view) buildBuiltinPkg(ctx context.Context) { pkg := pkgs[0] files := make(map[string]*ast.File) for _, filename := range pkg.GoFiles { - file, err := parser.ParseFile(cfg.Fset, filename, nil, parser.ParseComments) - if err != nil { + fh := v.session.GetFile(span.FileURI(filename)) + ph := v.session.cache.ParseGoHandle(fh, source.ParseFull) + file, _, err := ph.Parse(ctx) + if file == nil { + log.Error(ctx, "failed to parse builtin", err, telemetry.File.Of(filename)) v.builtinPkg, _ = ast.NewPackage(cfg.Fset, nil, nil, nil) return } diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go index ce04ca17fc..a2e2af8c70 100644 --- a/internal/lsp/cmd/cmd.go +++ b/internal/lsp/cmd/cmd.go @@ -344,7 +344,12 @@ func (c *cmdClient) getFile(ctx context.Context, uri span.URI) *cmdFile { } f := c.fset.AddFile(fname, -1, len(content)) f.SetLinesForContent(content) - file.mapper = protocol.NewColumnMapper(uri, fname, c.fset, f, content) + converter := span.NewContentConverter(fname, content) + file.mapper = &protocol.ColumnMapper{ + URI: uri, + Converter: converter, + Content: content, + } } return file } diff --git a/internal/lsp/link.go b/internal/lsp/link.go index 5c42f14ca0..9b248135f8 100644 --- a/internal/lsp/link.go +++ b/internal/lsp/link.go @@ -28,17 +28,10 @@ func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLink return nil, err } fh := f.Handle(ctx) - data, _, err := fh.Read(ctx) - if err != nil { - return nil, err - } - file, err := view.Session().Cache().ParseGoHandle(fh, source.ParseFull).Parse(ctx) + file, m, err := view.Session().Cache().ParseGoHandle(fh, source.ParseFull).Parse(ctx) if file == nil { return nil, err } - tok := view.Session().Cache().FileSet().File(file.Pos()) - m := protocol.NewColumnMapper(f.URI(), f.URI().Filename(), view.Session().Cache().FileSet(), tok, data) - var links []protocol.DocumentLink ast.Inspect(file, func(node ast.Node) bool { switch n := node.(type) { diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index 4bc9e354ac..1e0c5f263c 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -352,7 +352,7 @@ func (r *runner) FoldingRange(t *testing.T, data tests.FoldingRanges) { } func (r *runner) foldingRanges(t *testing.T, prefix string, uri span.URI, ranges []protocol.FoldingRange) { - m, err := r.mapper(uri) + m, err := r.data.Mapper(uri) if err != nil { t.Fatal(err) } @@ -484,7 +484,7 @@ func (r *runner) Format(t *testing.T, data tests.Formats) { } continue } - m, err := r.mapper(uri) + m, err := r.data.Mapper(uri) if err != nil { t.Fatal(err) } @@ -520,7 +520,7 @@ func (r *runner) Import(t *testing.T, data tests.Imports) { } continue } - m, err := r.mapper(uri) + m, err := r.data.Mapper(uri) if err != nil { t.Fatal(err) } @@ -572,7 +572,7 @@ func (r *runner) SuggestedFix(t *testing.T, data tests.SuggestedFixes) { } continue } - m, err := r.mapper(f.URI()) + m, err := r.data.Mapper(f.URI()) if err != nil { t.Fatal(err) } @@ -595,7 +595,7 @@ func (r *runner) SuggestedFix(t *testing.T, data tests.SuggestedFixes) { func (r *runner) Definition(t *testing.T, data tests.Definitions) { for _, d := range data { - sm, err := r.mapper(d.Src.URI()) + sm, err := r.data.Mapper(d.Src.URI()) if err != nil { t.Fatal(err) } @@ -648,7 +648,7 @@ func (r *runner) Definition(t *testing.T, data tests.Definitions) { } } else if !d.OnlyHover { locURI := span.NewURI(locs[0].URI) - lm, err := r.mapper(locURI) + lm, err := r.data.Mapper(locURI) if err != nil { t.Fatal(err) } @@ -665,7 +665,7 @@ func (r *runner) Definition(t *testing.T, data tests.Definitions) { func (r *runner) Highlight(t *testing.T, data tests.Highlights) { for name, locations := range data { - m, err := r.mapper(locations[0].URI()) + m, err := r.data.Mapper(locations[0].URI()) if err != nil { t.Fatal(err) } @@ -701,7 +701,7 @@ func (r *runner) Highlight(t *testing.T, data tests.Highlights) { func (r *runner) Reference(t *testing.T, data tests.References) { for src, itemList := range data { - sm, err := r.mapper(src.URI()) + sm, err := r.data.Mapper(src.URI()) if err != nil { t.Fatal(err) } @@ -712,7 +712,7 @@ func (r *runner) Reference(t *testing.T, data tests.References) { want := make(map[protocol.Location]bool) for _, pos := range itemList { - m, err := r.mapper(pos.URI()) + m, err := r.data.Mapper(pos.URI()) if err != nil { t.Fatal(err) } @@ -749,7 +749,7 @@ func (r *runner) Rename(t *testing.T, data tests.Renames) { uri := spn.URI() filename := uri.Filename() - sm, err := r.mapper(uri) + sm, err := r.data.Mapper(uri) if err != nil { t.Fatal(err) } @@ -777,7 +777,7 @@ func (r *runner) Rename(t *testing.T, data tests.Renames) { var res []string for uri, edits := range *workspaceEdits.Changes { - m, err := r.mapper(span.URI(uri)) + m, err := r.data.Mapper(span.URI(uri)) if err != nil { t.Fatal(err) } @@ -813,7 +813,7 @@ func (r *runner) Rename(t *testing.T, data tests.Renames) { func (r *runner) PrepareRename(t *testing.T, data tests.PrepareRenames) { for src, want := range data { - m, err := r.mapper(src.URI()) + m, err := r.data.Mapper(src.URI()) if err != nil { t.Fatal(err) } @@ -928,7 +928,7 @@ func summarizeSymbols(t *testing.T, i int, want []protocol.DocumentSymbol, got [ func (r *runner) SignatureHelp(t *testing.T, data tests.Signatures) { for spn, expectedSignatures := range data { - m, err := r.mapper(spn.URI()) + m, err := r.data.Mapper(spn.URI()) if err != nil { t.Fatal(err) } @@ -960,6 +960,9 @@ func (r *runner) SignatureHelp(t *testing.T, data tests.Signatures) { } continue } + if gotSignatures == nil { + t.Fatalf("expected %v, got nil", expectedSignatures) + } if diff := diffSignatures(spn, expectedSignatures, gotSignatures); diff != "" { t.Error(diff) } @@ -1003,7 +1006,7 @@ func diffSignatures(spn span.Span, want *source.SignatureInformation, got *proto func (r *runner) Link(t *testing.T, data tests.Links) { for uri, wantLinks := range data { - m, err := r.mapper(uri) + m, err := r.data.Mapper(uri) if err != nil { t.Fatal(err) } @@ -1054,27 +1057,6 @@ func (r *runner) Link(t *testing.T, data tests.Links) { } } -func (r *runner) mapper(uri span.URI) (*protocol.ColumnMapper, error) { - filename := uri.Filename() - fset := r.data.Exported.ExpectFileSet - var f *token.File - fset.Iterate(func(check *token.File) bool { - if check.Name() == filename { - f = check - return false - } - return true - }) - if f == nil { - return nil, fmt.Errorf("no token.File for %s", uri) - } - content, err := r.data.Exported.FileContents(f.Name()) - if err != nil { - return nil, err - } - return protocol.NewColumnMapper(uri, filename, fset, f, content), nil -} - func TestBytesOffset(t *testing.T) { tests := []struct { text string @@ -1102,7 +1084,13 @@ func TestBytesOffset(t *testing.T) { fset := token.NewFileSet() f := fset.AddFile(fname, -1, len(test.text)) f.SetLinesForContent([]byte(test.text)) - mapper := protocol.NewColumnMapper(span.FileURI(fname), fname, fset, f, []byte(test.text)) + uri := span.FileURI(fname) + converter := span.NewContentConverter(fname, []byte(test.text)) + mapper := &protocol.ColumnMapper{ + URI: uri, + Converter: converter, + Content: []byte(test.text), + } got, err := mapper.Point(test.pos) if err != nil && test.want != -1 { t.Errorf("unexpected error: %v", err) diff --git a/internal/lsp/protocol/span.go b/internal/lsp/protocol/span.go index feb25b038a..5c9c4d107d 100644 --- a/internal/lsp/protocol/span.go +++ b/internal/lsp/protocol/span.go @@ -8,7 +8,6 @@ package protocol import ( "fmt" - "go/token" "golang.org/x/tools/internal/span" errors "golang.org/x/xerrors" @@ -24,20 +23,6 @@ func NewURI(uri span.URI) string { return string(uri) } -func NewColumnMapper(uri span.URI, filename string, fset *token.FileSet, f *token.File, content []byte) *ColumnMapper { - var converter *span.TokenConverter - if f == nil { - converter = span.NewContentConverter(filename, content) - } else { - converter = span.NewTokenConverter(fset, f) - } - return &ColumnMapper{ - URI: uri, - Converter: converter, - Content: content, - } -} - func (m *ColumnMapper) Location(s span.Span) (Location, error) { rng, err := m.Range(s) if err != nil { diff --git a/internal/lsp/source/completion.go b/internal/lsp/source/completion.go index 8ece709bd5..c5a85c5488 100644 --- a/internal/lsp/source/completion.go +++ b/internal/lsp/source/completion.go @@ -388,20 +388,10 @@ func Completion(ctx context.Context, view View, f GoFile, pos protocol.Position, ph = h } } - file, err := ph.Cached(ctx) + file, m, err := ph.Cached(ctx) if file == nil { return nil, nil, err } - data, _, err := ph.File().Read(ctx) - if err != nil { - return nil, nil, err - } - fset := view.Session().Cache().FileSet() - tok := fset.File(file.Pos()) - if tok == nil { - return nil, nil, errors.Errorf("no token.File for %s", f.URI()) - } - m := protocol.NewColumnMapper(f.URI(), f.URI().Filename(), fset, tok, data) spn, err := m.PointSpan(pos) if err != nil { return nil, nil, err diff --git a/internal/lsp/source/completion_format.go b/internal/lsp/source/completion_format.go index 89d4f2a944..138b134dad 100644 --- a/internal/lsp/source/completion_format.go +++ b/internal/lsp/source/completion_format.go @@ -18,6 +18,7 @@ import ( "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/telemetry/log" "golang.org/x/tools/internal/telemetry/tag" + errors "golang.org/x/xerrors" ) // formatCompletion creates a completion item for a given candidate. @@ -124,10 +125,13 @@ func (c *completer) item(cand candidate) (CompletionItem, error) { } uri := span.FileURI(pos.Filename) - _, file, pkg, err := c.pkg.FindFile(c.ctx, uri, obj.Pos()) + _, file, pkg, err := c.pkg.FindFile(c.ctx, uri) if err != nil { return CompletionItem{}, err } + if !(file.Pos() <= obj.Pos() && obj.Pos() <= file.End()) { + return CompletionItem{}, errors.Errorf("no file for %s", obj.Name()) + } ident, err := findIdentifier(c.ctx, c.view, []Package{pkg}, file, obj.Pos()) if err != nil { return CompletionItem{}, err diff --git a/internal/lsp/source/diagnostics.go b/internal/lsp/source/diagnostics.go index 5cb5559b8b..76f4319ea8 100644 --- a/internal/lsp/source/diagnostics.go +++ b/internal/lsp/source/diagnostics.go @@ -190,29 +190,22 @@ func spanToRange(ctx context.Context, view View, pkg Package, spn span.Span, isT var ( fh FileHandle file *ast.File + m *protocol.ColumnMapper err error ) for _, ph := range pkg.GetHandles() { if ph.File().Identity().URI == spn.URI() { fh = ph.File() - file, err = ph.Cached(ctx) + file, m, err = ph.Cached(ctx) } } if file == nil { return protocol.Range{}, err } - fset := view.Session().Cache().FileSet() - tok := fset.File(file.Pos()) - if tok == nil { - return protocol.Range{}, errors.Errorf("no token.File for %s", spn.URI()) - } data, _, err := fh.Read(ctx) if err != nil { return protocol.Range{}, err } - uri := fh.Identity().URI - m := protocol.NewColumnMapper(uri, uri.Filename(), fset, tok, data) - // Try to get a range for the diagnostic. // TODO: Don't just limit ranges to type errors. if spn.IsPoint() && isTypeError { diff --git a/internal/lsp/source/folding_range.go b/internal/lsp/source/folding_range.go index 8a367098d0..f427979769 100644 --- a/internal/lsp/source/folding_range.go +++ b/internal/lsp/source/folding_range.go @@ -20,16 +20,11 @@ func FoldingRange(ctx context.Context, view View, f GoFile, lineFoldingOnly bool // TODO(suzmue): consider limiting the number of folding ranges returned, and // implement a way to prioritize folding ranges in that case. fh := f.Handle(ctx) - file, err := view.Session().Cache().ParseGoHandle(fh, ParseFull).Parse(ctx) + ph := view.Session().Cache().ParseGoHandle(fh, ParseFull) + file, m, err := ph.Parse(ctx) if err != nil { return nil, err } - data, _, err := fh.Read(ctx) - if err != nil { - return nil, err - } - fset := view.Session().Cache().FileSet() - m := protocol.NewColumnMapper(f.URI(), f.URI().Filename(), fset, fset.File(file.Pos()), data) // Get folding ranges for comments separately as they are not walked by ast.Inspect. ranges = append(ranges, commentsFoldingRange(view, m, file)...) diff --git a/internal/lsp/source/format.go b/internal/lsp/source/format.go index 033e546705..31722242ef 100644 --- a/internal/lsp/source/format.go +++ b/internal/lsp/source/format.go @@ -8,9 +8,7 @@ package source import ( "bytes" "context" - "go/ast" "go/format" - "go/token" "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/imports" @@ -34,12 +32,16 @@ func Format(ctx context.Context, view View, f File) ([]protocol.TextEdit, error) if err != nil { return nil, err } - var file *ast.File - for _, ph := range pkg.GetHandles() { - if ph.File().Identity().URI == f.URI() { - file, err = ph.Cached(ctx) + var ph ParseGoHandle + for _, h := range pkg.GetHandles() { + if h.File().Identity().URI == f.URI() { + ph = h } } + if ph == nil { + return nil, err + } + file, m, err := ph.Parse(ctx) if file == nil { return nil, err } @@ -52,7 +54,7 @@ func Format(ctx context.Context, view View, f File) ([]protocol.TextEdit, error) if err != nil { return nil, err } - return computeTextEdits(ctx, view.Session().Cache().FileSet(), f, string(formatted)) + return computeTextEdits(ctx, ph.File(), m, string(formatted)) } fset := view.Session().Cache().FileSet() @@ -65,7 +67,7 @@ func Format(ctx context.Context, view View, f File) ([]protocol.TextEdit, error) if err := format.Node(buf, fset, file); err != nil { return nil, err } - return computeTextEdits(ctx, view.Session().Cache().FileSet(), f, buf.String()) + return computeTextEdits(ctx, ph.File(), m, buf.String()) } func formatSource(ctx context.Context, file File) ([]byte, error) { @@ -82,10 +84,6 @@ func formatSource(ctx context.Context, file File) ([]byte, error) { func Imports(ctx context.Context, view View, f GoFile, rng span.Range) ([]protocol.TextEdit, error) { ctx, done := trace.StartSpan(ctx, "source.Imports") defer done() - data, _, err := f.Handle(ctx).Read(ctx) - if err != nil { - return nil, err - } pkg, err := f.GetPackage(ctx) if err != nil { return nil, err @@ -93,7 +91,15 @@ func Imports(ctx context.Context, view View, f GoFile, rng span.Range) ([]protoc if hasListErrors(pkg.GetErrors()) { return nil, errors.Errorf("%s has list errors, not running goimports", f.URI()) } - + var ph ParseGoHandle + for _, h := range pkg.GetHandles() { + if h.File().Identity().URI == f.URI() { + ph = h + } + } + if ph == nil { + return nil, err + } options := &imports.Options{ // Defaults. AllErrors: true, @@ -105,14 +111,22 @@ func Imports(ctx context.Context, view View, f GoFile, rng span.Range) ([]protoc } var formatted []byte importFn := func(opts *imports.Options) error { - formatted, err = imports.Process(f.URI().Filename(), data, opts) + data, _, err := ph.File().Read(ctx) + if err != nil { + return err + } + formatted, err = imports.Process(ph.File().Identity().URI.Filename(), data, opts) return err } err = view.RunProcessEnvFunc(ctx, importFn, options) if err != nil { return nil, err } - return computeTextEdits(ctx, view.Session().Cache().FileSet(), f, string(formatted)) + _, m, err := ph.Parse(ctx) + if m == nil { + return nil, err + } + return computeTextEdits(ctx, ph.File(), m, string(formatted)) } type ImportFix struct { @@ -132,11 +146,6 @@ func AllImportsFixes(ctx context.Context, view View, f File) (edits []protocol.T if !ok { return nil, nil, errors.Errorf("no imports fixes for non-Go files: %v", err) } - - data, _, err := f.Handle(ctx).Read(ctx) - if err != nil { - return nil, nil, err - } pkg, err := gof.GetPackage(ctx) if err != nil { return nil, nil, err @@ -154,6 +163,19 @@ func AllImportsFixes(ctx context.Context, view View, f File) (edits []protocol.T TabWidth: 8, } importFn := func(opts *imports.Options) error { + var ph ParseGoHandle + for _, h := range pkg.GetHandles() { + if h.File().Identity().URI == f.URI() { + ph = h + } + } + if ph == nil { + return errors.Errorf("no ParseGoHandle for %s", f.URI()) + } + data, _, err := ph.File().Read(ctx) + if err != nil { + return err + } fixes, err := imports.FixImports(f.URI().Filename(), data, opts) if err != nil { return err @@ -163,7 +185,11 @@ func AllImportsFixes(ctx context.Context, view View, f File) (edits []protocol.T if err != nil { return err } - edits, err = computeTextEdits(ctx, view.Session().Cache().FileSet(), f, string(formatted)) + _, m, err := ph.Parse(ctx) + if m == nil { + return err + } + edits, err = computeTextEdits(ctx, ph.File(), m, string(formatted)) if err != nil { return err } @@ -174,7 +200,7 @@ func AllImportsFixes(ctx context.Context, view View, f File) (edits []protocol.T if err != nil { return err } - edits, err := computeTextEdits(ctx, view.Session().Cache().FileSet(), f, string(formatted)) + edits, err := computeTextEdits(ctx, ph.File(), m, string(formatted)) if err != nil { return err } @@ -242,19 +268,16 @@ func hasListErrors(errors []packages.Error) bool { return false } -func computeTextEdits(ctx context.Context, fset *token.FileSet, f File, formatted string) ([]protocol.TextEdit, error) { +func computeTextEdits(ctx context.Context, fh FileHandle, m *protocol.ColumnMapper, formatted string) ([]protocol.TextEdit, error) { ctx, done := trace.StartSpan(ctx, "source.computeTextEdits") defer done() - data, _, err := f.Handle(ctx).Read(ctx) + data, _, err := fh.Read(ctx) if err != nil { return nil, err } - edits := diff.ComputeEdits(f.URI(), string(data), formatted) - m := protocol.NewColumnMapper(f.URI(), f.URI().Filename(), fset, nil, data) - + edits := diff.ComputeEdits(fh.Identity().URI, string(data), formatted) return ToProtocolEdits(m, edits) - } func ToProtocolEdits(m *protocol.ColumnMapper, edits []diff.TextEdit) ([]protocol.TextEdit, error) { diff --git a/internal/lsp/source/identifier.go b/internal/lsp/source/identifier.go index 993b82e4e0..8c63e4bfc0 100644 --- a/internal/lsp/source/identifier.go +++ b/internal/lsp/source/identifier.go @@ -226,10 +226,13 @@ func hasErrorType(obj types.Object) bool { func objToNode(ctx context.Context, view View, pkg Package, obj types.Object) (ast.Decl, error) { uri := span.FileURI(view.Session().Cache().FileSet().Position(obj.Pos()).Filename) - _, declAST, _, err := pkg.FindFile(ctx, uri, obj.Pos()) + _, declAST, _, err := pkg.FindFile(ctx, uri) if declAST == nil { return nil, err } + if !(declAST.Pos() <= obj.Pos() && obj.Pos() <= declAST.End()) { + return nil, errors.Errorf("no file for %s", obj.Name()) + } path, _ := astutil.PathEnclosingInterval(declAST, obj.Pos(), obj.Pos()) if path == nil { return nil, errors.Errorf("no path for object %v", obj.Name()) diff --git a/internal/lsp/source/rename.go b/internal/lsp/source/rename.go index e005a2dae7..6ba33314f4 100644 --- a/internal/lsp/source/rename.go +++ b/internal/lsp/source/rename.go @@ -178,7 +178,7 @@ func (i *IdentifierInfo) getPkgName(ctx context.Context) (*IdentifierInfo, error } for _, ph := range pkg.GetHandles() { if ph.File().Identity().URI == i.File.File().Identity().URI { - file, err = ph.Cached(ctx) + file, _, err = ph.Cached(ctx) } } if file == nil { diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go index 36de19759d..57c9a1ce35 100644 --- a/internal/lsp/source/source_test.go +++ b/internal/lsp/source/source_test.go @@ -488,10 +488,12 @@ func (r *runner) Format(t *testing.T, data tests.Formats) { } data, _, err := f.Handle(ctx).Read(ctx) if err != nil { - t.Error(err) - continue + t.Fatal(err) + } + m, err := r.data.Mapper(f.URI()) + if err != nil { + t.Fatal(err) } - m := protocol.NewColumnMapper(uri, filename, r.view.Session().Cache().FileSet(), nil, data) diffEdits, err := source.FromProtocolEdits(m, edits) if err != nil { t.Error(err) @@ -535,10 +537,12 @@ func (r *runner) Import(t *testing.T, data tests.Imports) { } data, _, err := fh.Read(ctx) if err != nil { - t.Error(err) - continue + t.Fatal(err) + } + m, err := r.data.Mapper(fh.Identity().URI) + if err != nil { + t.Fatal(err) } - m := protocol.NewColumnMapper(uri, filename, r.view.Session().Cache().FileSet(), nil, data) diffEdits, err := source.FromProtocolEdits(m, edits) if err != nil { t.Error(err) @@ -717,12 +721,15 @@ func (r *runner) Rename(t *testing.T, data tests.Renames) { if err != nil { t.Fatalf("failed for %v: %v", spn, err) } - data, _, err := f.Handle(ctx).Read(ctx) + fh := f.Handle(ctx) + data, _, err := fh.Read(ctx) if err != nil { - t.Error(err) - continue + t.Fatal(err) + } + m, err := r.data.Mapper(fh.Identity().URI) + if err != nil { + t.Fatal(err) } - m := protocol.NewColumnMapper(f.URI(), f.URI().Filename(), r.data.Exported.ExpectFileSet, nil, data) filename := filepath.Base(editSpn.Filename()) diffEdits, err := source.FromProtocolEdits(m, edits) if err != nil { @@ -922,13 +929,12 @@ func (r *runner) Link(t *testing.T, data tests.Links) { // This is a pure LSP feature, no source level functionality to be tested. } -func spanToRange(data *tests.Data, span span.Span) (*protocol.ColumnMapper, protocol.Range, error) { - contents, err := data.Exported.FileContents(span.URI().Filename()) +func spanToRange(data *tests.Data, spn span.Span) (*protocol.ColumnMapper, protocol.Range, error) { + m, err := data.Mapper(spn.URI()) if err != nil { return nil, protocol.Range{}, err } - m := protocol.NewColumnMapper(span.URI(), span.URI().Filename(), data.Exported.ExpectFileSet, nil, contents) - srcRng, err := m.Range(span) + srcRng, err := m.Range(spn) if err != nil { return nil, protocol.Range{}, err } diff --git a/internal/lsp/source/symbols.go b/internal/lsp/source/symbols.go index e0f2f32d65..1360ea1b30 100644 --- a/internal/lsp/source/symbols.go +++ b/internal/lsp/source/symbols.go @@ -25,7 +25,7 @@ func DocumentSymbols(ctx context.Context, view View, f GoFile) ([]protocol.Docum var file *ast.File for _, ph := range pkg.GetHandles() { if ph.File().Identity().URI == f.URI() { - file, err = ph.Cached(ctx) + file, _, err = ph.Cached(ctx) } } if file == nil { diff --git a/internal/lsp/source/util.go b/internal/lsp/source/util.go index 194159874d..3bdb955998 100644 --- a/internal/lsp/source/util.go +++ b/internal/lsp/source/util.go @@ -51,7 +51,7 @@ func (s mappedRange) URI() span.URI { return s.m.URI } -// bestCheckPackageHandle picks the "narrowest" package for a given file. +// bestPackage picks the "narrowest" package for a given file. // // By "narrowest" package, we mean the package with the fewest number of files // that includes the given file. This solves the problem of test variants, @@ -126,20 +126,11 @@ func pkgToMapper(ctx context.Context, view View, pkg Package, uri span.URI) (*as if ph == nil { return nil, nil, errors.Errorf("no ParseGoHandle for %s", uri) } - file, err := ph.Cached(ctx) + file, m, err := ph.Cached(ctx) if file == nil { return nil, nil, err } - data, _, err := ph.File().Read(ctx) - if err != nil { - return nil, nil, err - } - fset := view.Session().Cache().FileSet() - tok := fset.File(file.Pos()) - if tok == nil { - return nil, nil, errors.Errorf("no token.File for %s", uri) - } - return file, protocol.NewColumnMapper(uri, uri.Filename(), fset, tok, data), nil + return file, m, nil } func builtinFileToMapper(ctx context.Context, view View, f GoFile, file *ast.File) (*ast.File, *protocol.ColumnMapper, error) { @@ -148,12 +139,13 @@ func builtinFileToMapper(ctx context.Context, view View, f GoFile, file *ast.Fil if err != nil { return nil, nil, err } - fset := view.Session().Cache().FileSet() - tok := fset.File(file.Pos()) - if tok == nil { - return nil, nil, errors.Errorf("no token.File for %s", f.URI()) + converter := span.NewContentConverter(fh.Identity().URI.Filename(), data) + m := &protocol.ColumnMapper{ + URI: fh.Identity().URI, + Content: data, + Converter: converter, } - return nil, protocol.NewColumnMapper(f.URI(), f.URI().Filename(), fset, tok, data), nil + return nil, m, nil } func IsGenerated(ctx context.Context, view View, uri span.URI) bool { @@ -162,7 +154,7 @@ func IsGenerated(ctx context.Context, view View, uri span.URI) bool { return false } ph := view.Session().Cache().ParseGoHandle(f.Handle(ctx), ParseHeader) - parsed, err := ph.Parse(ctx) + parsed, _, err := ph.Parse(ctx) if parsed == nil { return false } diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index 1d8b61dae1..998a9c9575 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -80,10 +80,10 @@ type ParseGoHandle interface { // Parse returns the parsed AST for the file. // If the file is not available, returns nil and an error. - Parse(ctx context.Context) (*ast.File, error) + Parse(ctx context.Context) (*ast.File, *protocol.ColumnMapper, error) // Cached returns the AST for this handle, if it has already been stored. - Cached(ctx context.Context) (*ast.File, error) + Cached(ctx context.Context) (*ast.File, *protocol.ColumnMapper, error) } // ParseMode controls the content of the AST produced when parsing a source file. @@ -321,5 +321,5 @@ type Package interface { // FindFile returns the AST and type information for a file that may // belong to or be part of a dependency of the given package. - FindFile(ctx context.Context, uri span.URI, pos token.Pos) (ParseGoHandle, *ast.File, Package, error) + FindFile(ctx context.Context, uri span.URI) (ParseGoHandle, *ast.File, Package, error) } diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go index ffa49007de..9a97b039f8 100644 --- a/internal/lsp/tests/tests.go +++ b/internal/lsp/tests/tests.go @@ -15,6 +15,7 @@ import ( "path/filepath" "sort" "strings" + "sync" "testing" "golang.org/x/tools/go/expect" @@ -99,6 +100,9 @@ type Data struct { fragments map[string]string dir string golden map[string]*Golden + + mappersMu sync.Mutex + mappers map[span.URI]*protocol.ColumnMapper } type Tests interface { @@ -184,6 +188,7 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data { dir: dir, fragments: map[string]string{}, golden: map[string]*Golden{}, + mappers: map[span.URI]*protocol.ColumnMapper{}, } files := packagestest.MustCopyFileTree(dir) @@ -426,6 +431,25 @@ func Run(t *testing.T, tests Tests, data *Data) { } } +func (data *Data) Mapper(uri span.URI) (*protocol.ColumnMapper, error) { + data.mappersMu.Lock() + defer data.mappersMu.Unlock() + + if _, ok := data.mappers[uri]; !ok { + content, err := data.Exported.FileContents(uri.Filename()) + if err != nil { + return nil, err + } + converter := span.NewContentConverter(uri.Filename(), content) + data.mappers[uri] = &protocol.ColumnMapper{ + URI: uri, + Converter: converter, + Content: content, + } + } + return data.mappers[uri], nil +} + func (data *Data) Golden(tag string, target string, update func() ([]byte, error)) []byte { data.t.Helper() fragment, found := data.fragments[target] @@ -672,20 +696,18 @@ func (data *Data) collectPrepareRenames(src span.Span, rng span.Range, placehold // make the range just be the start. rng = span.NewRange(rng.FileSet, rng.Start, rng.Start) } - contents, err := data.Exported.FileContents(src.URI().Filename()) + m, err := data.Mapper(src.URI()) if err != nil { - return + data.t.Fatal(err) } - m := protocol.NewColumnMapper(src.URI(), src.URI().Filename(), data.Exported.ExpectFileSet, nil, contents) - // Convert range to span and then to protocol.Range. spn, err := rng.Span() if err != nil { - return + data.t.Fatal(err) } prng, err := m.Range(spn) if err != nil { - return + data.t.Fatal(err) } data.PrepareRenames[src] = &source.PrepareItem{ Range: prng, @@ -694,14 +716,13 @@ func (data *Data) collectPrepareRenames(src span.Span, rng span.Range, placehold } func (data *Data) collectSymbols(name string, spn span.Span, kind string, parentName string) { - contents, err := data.Exported.FileContents(spn.URI().Filename()) + m, err := data.Mapper(spn.URI()) if err != nil { - return + data.t.Fatal(err) } - m := protocol.NewColumnMapper(spn.URI(), spn.URI().Filename(), data.Exported.ExpectFileSet, nil, contents) rng, err := m.Range(spn) if err != nil { - return + data.t.Fatal(err) } sym := protocol.DocumentSymbol{ Name: name, diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go index 9f622b5e24..76ba2e8049 100644 --- a/internal/lsp/text_synchronization.go +++ b/internal/lsp/text_synchronization.go @@ -109,10 +109,14 @@ func (s *Server) applyChanges(ctx context.Context, uri span.URI, changes []proto if err != nil { return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found (%v)", err) } - fset := s.session.Cache().FileSet() for _, change := range changes { // Update column mapper along with the content. - m := protocol.NewColumnMapper(uri, uri.Filename(), fset, nil, content) + converter := span.NewContentConverter(uri.Filename(), content) + m := &protocol.ColumnMapper{ + URI: uri, + Converter: converter, + Content: content, + } spn, err := m.RangeSpan(*change.Range) if err != nil {