internal/lsp: add cache for type information

This change adds an additional cache for type information, which here is
just a *packages.Package for each package. The metadata cache maintains
the import graph, which allows us to easily determine when a package X
(and therefore any other package that imports X) should be invalidated.

Additionally, rather than performing content changes as they happen, we
queue up content changes and apply them the next time that any type
information is requested.

Updates golang/go#30309

Change-Id: Iaf569f641f84ce69b0c0d5bdabbaa85635eeb8bf
Reviewed-on: https://go-review.googlesource.com/c/tools/+/165438
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
This commit is contained in:
Rebecca Stambler 2019-03-05 17:30:44 -05:00
parent b40df0fb21
commit 00c44ba9c1
16 changed files with 256 additions and 155 deletions

View File

@ -1,6 +1,7 @@
package cache package cache
import ( import (
"context"
"fmt" "fmt"
"go/ast" "go/ast"
"go/parser" "go/parser"
@ -17,19 +18,29 @@ import (
"golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/source"
) )
func (v *View) parse(uri source.URI) error { func (v *View) parse(ctx context.Context, uri source.URI) error {
v.mcache.mu.Lock() v.mcache.mu.Lock()
defer v.mcache.mu.Unlock() defer v.mcache.mu.Unlock()
// Apply any queued-up content changes.
if err := v.applyContentChanges(ctx); err != nil {
return err
}
f := v.files[uri] f := v.files[uri]
// This should never happen. // This should never happen.
if f == nil { if f == nil {
return fmt.Errorf("no file for %v", uri) return fmt.Errorf("no file for %v", uri)
} }
// If the package for the file has not been invalidated by the application
imp, err := v.checkMetadata(f) // of the pending changes, there is no need to continue.
if err != nil { if f.isPopulated() {
return nil
}
// Check if the file's imports have changed. If they have, update the
// metadata by calling packages.Load.
if err := v.checkMetadata(ctx, f); err != nil {
return err return err
} }
if f.meta == nil { if f.meta == nil {
@ -37,10 +48,10 @@ func (v *View) parse(uri source.URI) error {
} }
// Start prefetching direct imports. // Start prefetching direct imports.
for importPath := range f.meta.children { for importPath := range f.meta.children {
go imp.Import(importPath) go v.Import(importPath)
} }
// Type-check package. // Type-check package.
pkg, err := imp.typeCheck(f.meta.pkgPath) pkg, err := v.typeCheck(f.meta.pkgPath)
if pkg == nil || pkg.Types == nil { if pkg == nil || pkg.Types == nil {
return err return err
} }
@ -75,29 +86,12 @@ func (v *View) cachePackage(pkg *packages.Package) {
} }
} }
type importer struct { func (v *View) checkMetadata(ctx context.Context, f *File) error {
mu sync.Mutex
entries map[string]*entry
*View
}
type entry struct {
pkg *packages.Package
err error
ready chan struct{}
}
func (v *View) checkMetadata(f *File) (*importer, error) {
filename, err := f.URI.Filename() filename, err := f.URI.Filename()
if err != nil { if err != nil {
return nil, err return err
} }
imp := &importer{ if v.reparseImports(ctx, f, filename) {
View: v,
entries: make(map[string]*entry),
}
if v.reparseImports(f, filename) {
cfg := v.Config cfg := v.Config
cfg.Mode = packages.LoadImports cfg.Mode = packages.LoadImports
pkgs, err := packages.Load(&cfg, fmt.Sprintf("file=%s", filename)) pkgs, err := packages.Load(&cfg, fmt.Sprintf("file=%s", filename))
@ -105,7 +99,7 @@ func (v *View) checkMetadata(f *File) (*importer, error) {
if err == nil { if err == nil {
err = fmt.Errorf("no packages found for %s", filename) err = fmt.Errorf("no packages found for %s", filename)
} }
return nil, err return err
} }
for _, pkg := range pkgs { for _, pkg := range pkgs {
// If the package comes back with errors from `go list`, don't bother // If the package comes back with errors from `go list`, don't bother
@ -113,23 +107,23 @@ func (v *View) checkMetadata(f *File) (*importer, error) {
for _, err := range pkg.Errors { for _, err := range pkg.Errors {
switch err.Kind { switch err.Kind {
case packages.UnknownError, packages.ListError: case packages.UnknownError, packages.ListError:
return nil, err return err
} }
} }
v.link(pkg.PkgPath, pkg, nil) v.link(pkg.PkgPath, pkg, nil)
} }
} }
return imp, nil return nil
} }
// reparseImports reparses a file's import declarations to determine if they // reparseImports reparses a file's import declarations to determine if they
// have changed. // have changed.
func (v *View) reparseImports(f *File, filename string) bool { func (v *View) reparseImports(ctx context.Context, f *File, filename string) bool {
if f.meta == nil { if f.meta == nil {
return true return true
} }
// Get file content in case we don't already have it? // Get file content in case we don't already have it?
f.read() f.read(ctx)
parsed, _ := parser.ParseFile(v.Config.Fset, filename, f.content, parser.ImportsOnly) parsed, _ := parser.ParseFile(v.Config.Fset, filename, f.content, parser.ImportsOnly)
if parsed == nil { if parsed == nil {
return true return true
@ -186,23 +180,23 @@ func (v *View) link(pkgPath string, pkg *packages.Package, parent *metadata) *me
return m return m
} }
func (imp *importer) Import(pkgPath string) (*types.Package, error) { func (v *View) Import(pkgPath string) (*types.Package, error) {
imp.mu.Lock() v.pcache.mu.Lock()
e, ok := imp.entries[pkgPath] e, ok := v.pcache.packages[pkgPath]
if ok { if ok {
// cache hit // cache hit
imp.mu.Unlock() v.pcache.mu.Unlock()
// wait for entry to become ready // wait for entry to become ready
<-e.ready <-e.ready
} else { } else {
// cache miss // cache miss
e = &entry{ready: make(chan struct{})} e = &entry{ready: make(chan struct{})}
imp.entries[pkgPath] = e v.pcache.packages[pkgPath] = e
imp.mu.Unlock() v.pcache.mu.Unlock()
// This goroutine becomes responsible for populating // This goroutine becomes responsible for populating
// the entry and broadcasting its readiness. // the entry and broadcasting its readiness.
e.pkg, e.err = imp.typeCheck(pkgPath) e.pkg, e.err = v.typeCheck(pkgPath)
close(e.ready) close(e.ready)
} }
if e.err != nil { if e.err != nil {
@ -211,8 +205,8 @@ func (imp *importer) Import(pkgPath string) (*types.Package, error) {
return e.pkg.Types, nil return e.pkg.Types, nil
} }
func (imp *importer) typeCheck(pkgPath string) (*packages.Package, error) { func (v *View) typeCheck(pkgPath string) (*packages.Package, error) {
meta, ok := imp.mcache.packages[pkgPath] meta, ok := v.mcache.packages[pkgPath]
if !ok { if !ok {
return nil, fmt.Errorf("no metadata for %v", pkgPath) return nil, fmt.Errorf("no metadata for %v", pkgPath)
} }
@ -229,7 +223,7 @@ func (imp *importer) typeCheck(pkgPath string) (*packages.Package, error) {
PkgPath: meta.pkgPath, PkgPath: meta.pkgPath,
CompiledGoFiles: meta.files, CompiledGoFiles: meta.files,
Imports: make(map[string]*packages.Package), Imports: make(map[string]*packages.Package),
Fset: imp.Config.Fset, Fset: v.Config.Fset,
Types: typ, Types: typ,
TypesInfo: &types.Info{ TypesInfo: &types.Info{
Types: make(map[ast.Expr]types.TypeAndValue), Types: make(map[ast.Expr]types.TypeAndValue),
@ -243,24 +237,35 @@ func (imp *importer) typeCheck(pkgPath string) (*packages.Package, error) {
TypesSizes: &types.StdSizes{}, TypesSizes: &types.StdSizes{},
} }
appendError := func(err error) { appendError := func(err error) {
imp.appendPkgError(pkg, err) v.appendPkgError(pkg, err)
} }
files, errs := imp.parseFiles(pkg.CompiledGoFiles) files, errs := v.parseFiles(meta.files)
for _, err := range errs { for _, err := range errs {
appendError(err) appendError(err)
} }
pkg.Syntax = files pkg.Syntax = files
cfg := &types.Config{ cfg := &types.Config{
Error: appendError, Error: appendError,
Importer: imp, Importer: v,
} }
check := types.NewChecker(cfg, imp.Config.Fset, pkg.Types, pkg.TypesInfo) check := types.NewChecker(cfg, v.Config.Fset, pkg.Types, pkg.TypesInfo)
check.Files(pkg.Syntax) check.Files(pkg.Syntax)
// Set imports of package to correspond to cached packages. This is
// necessary for go/analysis, but once we merge its approach with the
// current caching system, we can eliminate this.
v.pcache.mu.Lock()
for importPath := range meta.children {
if importEntry, ok := v.pcache.packages[importPath]; ok {
pkg.Imports[importPath] = importEntry.pkg
}
}
v.pcache.mu.Unlock()
return pkg, nil return pkg, nil
} }
func (imp *importer) appendPkgError(pkg *packages.Package, err error) { func (v *View) appendPkgError(pkg *packages.Package, err error) {
if err == nil { if err == nil {
return return
} }
@ -283,7 +288,7 @@ func (imp *importer) appendPkgError(pkg *packages.Package, err error) {
} }
case types.Error: case types.Error:
errs = append(errs, packages.Error{ errs = append(errs, packages.Error{
Pos: imp.Config.Fset.Position(err.Pos).String(), Pos: v.Config.Fset.Position(err.Pos).String(),
Msg: err.Msg, Msg: err.Msg,
Kind: packages.TypeError, Kind: packages.TypeError,
}) })

View File

@ -5,6 +5,7 @@
package cache package cache
import ( import (
"context"
"go/ast" "go/ast"
"go/token" "go/token"
"io/ioutil" "io/ioutil"
@ -27,44 +28,51 @@ type File struct {
} }
// GetContent returns the contents of the file, reading it from file system if needed. // GetContent returns the contents of the file, reading it from file system if needed.
func (f *File) GetContent() []byte { func (f *File) GetContent(ctx context.Context) []byte {
f.view.mu.Lock() f.view.mu.Lock()
defer f.view.mu.Unlock() defer f.view.mu.Unlock()
f.read()
if ctx.Err() == nil {
f.read(ctx)
}
return f.content return f.content
} }
func (f *File) GetFileSet() *token.FileSet { func (f *File) GetFileSet(ctx context.Context) *token.FileSet {
return f.view.Config.Fset return f.view.Config.Fset
} }
func (f *File) GetToken() *token.File { func (f *File) GetToken(ctx context.Context) *token.File {
f.view.mu.Lock() f.view.mu.Lock()
defer f.view.mu.Unlock() defer f.view.mu.Unlock()
if f.token == nil {
if err := f.view.parse(f.URI); err != nil { if f.token == nil || len(f.view.contentChanges) > 0 {
if err := f.view.parse(ctx, f.URI); err != nil {
return nil return nil
} }
} }
return f.token return f.token
} }
func (f *File) GetAST() *ast.File { func (f *File) GetAST(ctx context.Context) *ast.File {
f.view.mu.Lock() f.view.mu.Lock()
defer f.view.mu.Unlock() defer f.view.mu.Unlock()
if f.ast == nil {
if err := f.view.parse(f.URI); err != nil { if f.ast == nil || len(f.view.contentChanges) > 0 {
if err := f.view.parse(ctx, f.URI); err != nil {
return nil return nil
} }
} }
return f.ast return f.ast
} }
func (f *File) GetPackage() *packages.Package { func (f *File) GetPackage(ctx context.Context) *packages.Package {
f.view.mu.Lock() f.view.mu.Lock()
defer f.view.mu.Unlock() defer f.view.mu.Unlock()
if f.pkg == nil {
if err := f.view.parse(f.URI); err != nil { if f.pkg == nil || len(f.view.contentChanges) > 0 {
if err := f.view.parse(ctx, f.URI); err != nil {
return nil return nil
} }
} }
@ -73,10 +81,20 @@ func (f *File) GetPackage() *packages.Package {
// read is the internal part of GetContent. It assumes that the caller is // read is the internal part of GetContent. It assumes that the caller is
// holding the mutex of the file's view. // holding the mutex of the file's view.
func (f *File) read() { func (f *File) read(ctx context.Context) {
if f.content != nil { if f.content != nil {
if len(f.view.contentChanges) == 0 {
return return
} }
f.view.mcache.mu.Lock()
err := f.view.applyContentChanges(ctx)
f.view.mcache.mu.Unlock()
if err == nil {
return
}
}
// We don't know the content yet, so read it. // We don't know the content yet, so read it.
filename, err := f.URI.Filename() filename, err := f.URI.Filename()
if err != nil { if err != nil {
@ -88,3 +106,8 @@ func (f *File) read() {
} }
f.content = content f.content = content
} }
// isPopulated returns true if all of the computed fields of the file are set.
func (f *File) isPopulated() bool {
return f.ast != nil && f.token != nil && f.pkg != nil && f.meta != nil && f.imports != nil
}

View File

@ -17,6 +17,14 @@ type View struct {
// mu protects all mutable state of the view. // mu protects all mutable state of the view.
mu sync.Mutex mu sync.Mutex
// backgroundCtx is the current context used by background tasks initiated
// by the view.
backgroundCtx context.Context
// cancel is called when all action being performed by the current view
// should be stopped.
cancel context.CancelFunc
// Config is the configuration used for the view's interaction with the // Config is the configuration used for the view's interaction with the
// go/packages API. It is shared across all views. // go/packages API. It is shared across all views.
Config packages.Config Config packages.Config
@ -24,9 +32,18 @@ type View struct {
// files caches information for opened files in a view. // files caches information for opened files in a view.
files map[source.URI]*File files map[source.URI]*File
// 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
// are applied, potentially invalidating some data in the caches. The
// closures in the dirty slice assume that their caller is holding the
// view's mutex.
contentChanges map[source.URI]func()
// mcache caches metadata for the packages of the opened files in a view. // mcache caches metadata for the packages of the opened files in a view.
mcache *metadataCache mcache *metadataCache
pcache *packageCache
analysisCache *source.AnalysisCache analysisCache *source.AnalysisCache
} }
@ -41,16 +58,42 @@ type metadata struct {
parents, children map[string]bool parents, children map[string]bool
} }
type packageCache struct {
mu sync.Mutex
packages map[string]*entry
}
type entry struct {
pkg *packages.Package
err error
ready chan struct{} // closed to broadcast ready condition
}
func NewView(config *packages.Config) *View { func NewView(config *packages.Config) *View {
ctx, cancel := context.WithCancel(context.Background())
return &View{ return &View{
backgroundCtx: ctx,
cancel: cancel,
Config: *config, Config: *config,
files: make(map[source.URI]*File), files: make(map[source.URI]*File),
contentChanges: make(map[source.URI]func()),
mcache: &metadataCache{ mcache: &metadataCache{
packages: make(map[string]*metadata), packages: make(map[string]*metadata),
}, },
pcache: &packageCache{
packages: make(map[string]*entry),
},
} }
} }
func (v *View) BackgroundContext() context.Context {
v.mu.Lock()
defer v.mu.Unlock()
return v.backgroundCtx
}
func (v *View) FileSet() *token.FileSet { func (v *View) FileSet() *token.FileSet {
return v.Config.Fset return v.Config.Fset
} }
@ -60,45 +103,57 @@ func (v *View) GetAnalysisCache() *source.AnalysisCache {
return v.analysisCache return v.analysisCache
} }
func (v *View) copyView() *View { // SetContent sets the overlay contents for a file.
return &View{ func (v *View) SetContent(ctx context.Context, uri source.URI, content []byte) error {
Config: v.Config,
files: make(map[source.URI]*File),
mcache: v.mcache,
}
}
// SetContent sets the overlay contents for a file. A nil content value will
// remove the file from the active set and revert it to its on-disk contents.
func (v *View) SetContent(ctx context.Context, uri source.URI, content []byte) (source.View, error) {
v.mu.Lock() v.mu.Lock()
defer v.mu.Unlock() defer v.mu.Unlock()
newView := v.copyView() // Cancel all still-running previous requests, since they would be
// operating on stale data.
v.cancel()
v.backgroundCtx, v.cancel = context.WithCancel(context.Background())
for fURI, f := range v.files { v.contentChanges[uri] = func() {
newView.files[fURI] = &File{ v.applyContentChange(uri, content)
URI: fURI,
view: newView,
active: f.active,
content: f.content,
ast: f.ast,
token: f.token,
pkg: f.pkg,
meta: f.meta,
imports: f.imports,
}
} }
f := newView.getFile(uri) return nil
}
// applyContentChanges applies all of the changed content stored in the view.
// It is assumed that the caller has locked both the view's and the mcache's
// mutexes.
func (v *View) applyContentChanges(ctx context.Context) error {
if ctx.Err() != nil {
return ctx.Err()
}
v.pcache.mu.Lock()
defer v.pcache.mu.Unlock()
for uri, change := range v.contentChanges {
change()
delete(v.contentChanges, uri)
}
return nil
}
// setContent applies a content update for a given file. It assumes that the
// caller is holding the view's mutex.
func (v *View) applyContentChange(uri source.URI, content []byte) {
f := v.getFile(uri)
f.content = content f.content = content
// Resetting the contents invalidates the ast, token, and pkg fields. // TODO(rstambler): Should we recompute these here?
f.ast = nil f.ast = nil
f.token = nil f.token = nil
f.pkg = nil
// We might need to update the overlay. // Remove the package and all of its reverse dependencies from the cache.
if f.pkg != nil {
v.remove(f.pkg.PkgPath)
}
switch { switch {
case f.active && content == nil: case f.active && content == nil:
// The file was active, so we need to forget its content. // The file was active, so we need to forget its content.
@ -114,17 +169,40 @@ func (v *View) SetContent(ctx context.Context, uri source.URI, content []byte) (
f.view.Config.Overlay[filename] = f.content f.view.Config.Overlay[filename] = f.content
} }
} }
}
return newView, nil // remove invalidates a package and its reverse dependencies in the view's
// package cache. It is assumed that the caller has locked both the mutexes
// of both the mcache and the pcache.
func (v *View) remove(pkgPath string) {
m, ok := v.mcache.packages[pkgPath]
if !ok {
return
}
for parentPkgPath := range m.parents {
v.remove(parentPkgPath)
}
// All of the files in the package may also be holding a pointer to the
// invalidated package.
for _, filename := range m.files {
if f, ok := v.files[source.ToURI(filename)]; ok {
f.pkg = nil
}
}
delete(v.pcache.packages, pkgPath)
} }
// GetFile returns a File for the given URI. It will always succeed because it // GetFile returns a File for the given URI. It will always succeed because it
// adds the file to the managed set if needed. // adds the file to the managed set if needed.
func (v *View) GetFile(ctx context.Context, uri source.URI) (source.File, error) { func (v *View) GetFile(ctx context.Context, uri source.URI) (source.File, error) {
v.mu.Lock() v.mu.Lock()
f := v.getFile(uri) defer v.mu.Unlock()
v.mu.Unlock()
return f, nil if ctx.Err() != nil {
return nil, ctx.Err()
}
return v.getFile(uri), nil
} }
// getFile is the unlocked internal implementation of GetFile. // getFile is the unlocked internal implementation of GetFile.

View File

@ -65,7 +65,7 @@ func (d *definition) Run(ctx context.Context, args ...string) error {
if err != nil { if err != nil {
return err return err
} }
tok := f.GetToken() tok := f.GetToken(ctx)
pos := tok.Pos(from.Start.Offset) pos := tok.Pos(from.Start.Offset)
if !pos.IsValid() { if !pos.IsValid() {
return fmt.Errorf("invalid position %v", from.Start.Offset) return fmt.Errorf("invalid position %v", from.Start.Offset)
@ -80,9 +80,9 @@ func (d *definition) Run(ctx context.Context, args ...string) error {
var result interface{} var result interface{}
switch d.query.Emulate { switch d.query.Emulate {
case "": case "":
result, err = buildDefinition(view, ident) result, err = buildDefinition(ctx, view, ident)
case emulateGuru: case emulateGuru:
result, err = buildGuruDefinition(view, ident) result, err = buildGuruDefinition(ctx, view, ident)
default: default:
return fmt.Errorf("unknown emulation for definition: %s", d.query.Emulate) return fmt.Errorf("unknown emulation for definition: %s", d.query.Emulate)
} }
@ -105,8 +105,8 @@ func (d *definition) Run(ctx context.Context, args ...string) error {
return nil return nil
} }
func buildDefinition(view source.View, ident *source.IdentifierInfo) (*Definition, error) { func buildDefinition(ctx context.Context, view source.View, ident *source.IdentifierInfo) (*Definition, error) {
content, err := ident.Hover(nil) content, err := ident.Hover(ctx, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -116,9 +116,9 @@ func buildDefinition(view source.View, ident *source.IdentifierInfo) (*Definitio
}, nil }, nil
} }
func buildGuruDefinition(view source.View, ident *source.IdentifierInfo) (*guru.Definition, error) { func buildGuruDefinition(ctx context.Context, view source.View, ident *source.IdentifierInfo) (*guru.Definition, error) {
loc := newLocation(view.FileSet(), ident.Declaration.Range) loc := newLocation(view.FileSet(), ident.Declaration.Range)
pkg := ident.File.GetPackage() pkg := ident.File.GetPackage(ctx)
// guru does not support ranges // guru does not support ranges
loc.End = loc.Start loc.End = loc.Start
// Behavior that attempts to match the expected output for guru. For an example // Behavior that attempts to match the expected output for guru. For an example

View File

@ -21,6 +21,10 @@ func (s *server) cacheAndDiagnose(ctx context.Context, uri protocol.DocumentURI,
return // handle error? return // handle error?
} }
go func() { go func() {
ctx := s.view.BackgroundContext()
if ctx.Err() != nil {
return
}
reports, err := source.Diagnostics(ctx, s.view, sourceURI) reports, err := source.Diagnostics(ctx, s.view, sourceURI)
if err != nil { if err != nil {
return // handle error? return // handle error?
@ -35,16 +39,7 @@ func (s *server) cacheAndDiagnose(ctx context.Context, uri protocol.DocumentURI,
} }
func (s *server) setContent(ctx context.Context, uri source.URI, content []byte) error { func (s *server) setContent(ctx context.Context, uri source.URI, content []byte) error {
v, err := s.view.SetContent(ctx, uri, content) return s.view.SetContent(ctx, uri, content)
if err != nil {
return err
}
s.viewMu.Lock()
s.view = v
s.viewMu.Unlock()
return nil
} }
func toProtocolDiagnostics(ctx context.Context, v source.View, diagnostics []source.Diagnostic) []protocol.Diagnostic { func toProtocolDiagnostics(ctx context.Context, v source.View, diagnostics []source.Diagnostic) []protocol.Diagnostic {

View File

@ -17,7 +17,7 @@ func formatRange(ctx context.Context, v source.View, uri protocol.DocumentURI, r
if err != nil { if err != nil {
return nil, err return nil, err
} }
tok := f.GetToken() tok := f.GetToken(ctx)
var r source.Range var r source.Range
if rng == nil { if rng == nil {
r.Start = tok.Pos(0) r.Start = tok.Pos(0)
@ -29,15 +29,15 @@ func formatRange(ctx context.Context, v source.View, uri protocol.DocumentURI, r
if err != nil { if err != nil {
return nil, err return nil, err
} }
return toProtocolEdits(f, edits), nil return toProtocolEdits(ctx, f, edits), nil
} }
func toProtocolEdits(f source.File, edits []source.TextEdit) []protocol.TextEdit { func toProtocolEdits(ctx context.Context, f source.File, edits []source.TextEdit) []protocol.TextEdit {
if edits == nil { if edits == nil {
return nil return nil
} }
tok := f.GetToken() tok := f.GetToken(ctx)
content := f.GetContent() content := f.GetContent(ctx)
// When a file ends with an empty line, the newline character is counted // When a file ends with an empty line, the newline character is counted
// as part of the previous line. This causes the formatter to insert // as part of the previous line. This causes the formatter to insert
// another unnecessary newline on each formatting. We handle this case by // another unnecessary newline on each formatting. We handle this case by

View File

@ -20,7 +20,7 @@ func organizeImports(ctx context.Context, v source.View, uri protocol.DocumentUR
if err != nil { if err != nil {
return nil, err return nil, err
} }
tok := f.GetToken() tok := f.GetToken(ctx)
r := source.Range{ r := source.Range{
Start: tok.Pos(0), Start: tok.Pos(0),
End: tok.Pos(tok.Size()), End: tok.Pos(tok.Size()),
@ -29,5 +29,5 @@ func organizeImports(ctx context.Context, v source.View, uri protocol.DocumentUR
if err != nil { if err != nil {
return nil, err return nil, err
} }
return toProtocolEdits(f, edits), nil return toProtocolEdits(ctx, f, edits), nil
} }

View File

@ -400,7 +400,7 @@ func (f formats) test(t *testing.T, s *server) {
}) })
} }
} }
split := strings.SplitAfter(string(f.GetContent()), "\n") split := strings.SplitAfter(string(f.GetContent(context.Background())), "\n")
got := strings.Join(diff.ApplyEdits(split, ops), "") got := strings.Join(diff.ApplyEdits(split, ops), "")
if gofmted != got { if gofmted != got {
t.Errorf("format failed for %s: expected '%v', got '%v'", filename, gofmted, got) t.Errorf("format failed for %s: expected '%v', got '%v'", filename, gofmted, got)

View File

@ -36,7 +36,7 @@ func fromProtocolLocation(ctx context.Context, v *cache.View, loc protocol.Locat
if err != nil { if err != nil {
return source.Range{}, err return source.Range{}, err
} }
tok := f.GetToken() tok := f.GetToken(ctx)
return fromProtocolRange(tok, loc.Range), nil return fromProtocolRange(tok, loc.Range), nil
} }

View File

@ -72,7 +72,7 @@ type server struct {
textDocumentSyncKind protocol.TextDocumentSyncKind textDocumentSyncKind protocol.TextDocumentSyncKind
viewMu sync.Mutex viewMu sync.Mutex
view source.View view *cache.View
} }
func (s *server) Initialize(ctx context.Context, params *protocol.InitializeParams) (*protocol.InitializeResult, error) { func (s *server) Initialize(ctx context.Context, params *protocol.InitializeParams) (*protocol.InitializeResult, error) {
@ -233,7 +233,7 @@ func (s *server) applyChanges(ctx context.Context, params *protocol.DidChangeTex
return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found") return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found")
} }
content := file.GetContent() content := file.GetContent(ctx)
for _, change := range params.ContentChanges { for _, change := range params.ContentChanges {
start := bytesOffset(content, change.Range.Start) start := bytesOffset(content, change.Range.Start)
if start == -1 { if start == -1 {
@ -307,7 +307,7 @@ func (s *server) Completion(ctx context.Context, params *protocol.CompletionPara
if err != nil { if err != nil {
return nil, err return nil, err
} }
tok := f.GetToken() tok := f.GetToken(ctx)
pos := fromProtocolPosition(tok, params.Position) pos := fromProtocolPosition(tok, params.Position)
items, prefix, err := source.Completion(ctx, f, pos) items, prefix, err := source.Completion(ctx, f, pos)
if err != nil { if err != nil {
@ -332,13 +332,13 @@ func (s *server) Hover(ctx context.Context, params *protocol.TextDocumentPositio
if err != nil { if err != nil {
return nil, err return nil, err
} }
tok := f.GetToken() tok := f.GetToken(ctx)
pos := fromProtocolPosition(tok, params.Position) pos := fromProtocolPosition(tok, params.Position)
ident, err := source.Identifier(ctx, s.view, f, pos) ident, err := source.Identifier(ctx, s.view, f, pos)
if err != nil { if err != nil {
return nil, err return nil, err
} }
content, err := ident.Hover(nil) content, err := ident.Hover(ctx, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -361,7 +361,7 @@ func (s *server) SignatureHelp(ctx context.Context, params *protocol.TextDocumen
if err != nil { if err != nil {
return nil, err return nil, err
} }
tok := f.GetToken() tok := f.GetToken(ctx)
pos := fromProtocolPosition(tok, params.Position) pos := fromProtocolPosition(tok, params.Position)
info, err := source.SignatureHelp(ctx, f, pos) info, err := source.SignatureHelp(ctx, f, pos)
if err != nil { if err != nil {
@ -379,7 +379,7 @@ func (s *server) Definition(ctx context.Context, params *protocol.TextDocumentPo
if err != nil { if err != nil {
return nil, err return nil, err
} }
tok := f.GetToken() tok := f.GetToken(ctx)
pos := fromProtocolPosition(tok, params.Position) pos := fromProtocolPosition(tok, params.Position)
ident, err := source.Identifier(ctx, s.view, f, pos) ident, err := source.Identifier(ctx, s.view, f, pos)
if err != nil { if err != nil {
@ -397,7 +397,7 @@ func (s *server) TypeDefinition(ctx context.Context, params *protocol.TextDocume
if err != nil { if err != nil {
return nil, err return nil, err
} }
tok := f.GetToken() tok := f.GetToken(ctx)
pos := fromProtocolPosition(tok, params.Position) pos := fromProtocolPosition(tok, params.Position)
ident, err := source.Identifier(ctx, s.view, f, pos) ident, err := source.Identifier(ctx, s.view, f, pos)
if err != nil { if err != nil {

View File

@ -47,8 +47,8 @@ type finder func(types.Object, float64, []CompletionItem) []CompletionItem
// completion. For instance, some clients may tolerate imperfect matches as // completion. For instance, some clients may tolerate imperfect matches as
// valid completion results, since users may make typos. // valid completion results, since users may make typos.
func Completion(ctx context.Context, f File, pos token.Pos) (items []CompletionItem, prefix string, err error) { func Completion(ctx context.Context, f File, pos token.Pos) (items []CompletionItem, prefix string, err error) {
file := f.GetAST() file := f.GetAST(ctx)
pkg := f.GetPackage() pkg := f.GetPackage(ctx)
path, _ := astutil.PathEnclosingInterval(file, pos, pos) path, _ := astutil.PathEnclosingInterval(file, pos, pos)
if path == nil { if path == nil {
return nil, "", fmt.Errorf("cannot find node enclosing position") return nil, "", fmt.Errorf("cannot find node enclosing position")

View File

@ -48,10 +48,10 @@ func Identifier(ctx context.Context, v View, f File, pos token.Pos) (*Identifier
return result, err return result, err
} }
func (i *IdentifierInfo) Hover(q types.Qualifier) (string, error) { func (i *IdentifierInfo) Hover(ctx context.Context, q types.Qualifier) (string, error) {
if q == nil { if q == nil {
fAST := i.File.GetAST() fAST := i.File.GetAST(ctx)
pkg := i.File.GetPackage() pkg := i.File.GetPackage(ctx)
q = qualifier(fAST, pkg.Types, pkg.TypesInfo) q = qualifier(fAST, pkg.Types, pkg.TypesInfo)
} }
return types.ObjectString(i.Declaration.Object, q), nil return types.ObjectString(i.Declaration.Object, q), nil
@ -59,8 +59,8 @@ func (i *IdentifierInfo) Hover(q types.Qualifier) (string, error) {
// identifier checks a single position for a potential identifier. // 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 File, pos token.Pos) (*IdentifierInfo, error) {
fAST := f.GetAST() fAST := f.GetAST(ctx)
pkg := f.GetPackage() pkg := f.GetPackage(ctx)
path, _ := astutil.PathEnclosingInterval(fAST, pos, pos) path, _ := astutil.PathEnclosingInterval(fAST, pos, pos)
result := &IdentifierInfo{ result := &IdentifierInfo{
File: f, File: f,

View File

@ -58,7 +58,7 @@ func Diagnostics(ctx context.Context, v View, uri URI) (map[string][]Diagnostic,
if err != nil { if err != nil {
return nil, err return nil, err
} }
pkg := f.GetPackage() pkg := f.GetPackage(ctx)
// Prepare the reports we will send for this package. // Prepare the reports we will send for this package.
reports := make(map[string][]Diagnostic) reports := make(map[string][]Diagnostic)
for _, filename := range pkg.CompiledGoFiles { for _, filename := range pkg.CompiledGoFiles {
@ -87,8 +87,8 @@ func Diagnostics(ctx context.Context, v View, uri URI) (map[string][]Diagnostic,
if err != nil { if err != nil {
continue continue
} }
diagTok := diagFile.GetToken() diagTok := diagFile.GetToken(ctx)
end, err := identifierEnd(diagFile.GetContent(), pos.Line, pos.Column) end, err := identifierEnd(diagFile.GetContent(ctx), pos.Line, pos.Column)
// Don't set a range if it's anything other than a type error. // Don't set a range if it's anything other than a type error.
if err != nil || diag.Kind != packages.TypeError { if err != nil || diag.Kind != packages.TypeError {
end = 0 end = 0

View File

@ -21,7 +21,7 @@ import (
// Format formats a file with a given range. // Format formats a file with a given range.
func Format(ctx context.Context, f File, rng Range) ([]TextEdit, error) { func Format(ctx context.Context, f File, rng Range) ([]TextEdit, error) {
fAST := f.GetAST() fAST := f.GetAST(ctx)
path, exact := astutil.PathEnclosingInterval(fAST, rng.Start, rng.End) path, exact := astutil.PathEnclosingInterval(fAST, rng.Start, rng.End)
if !exact || len(path) == 0 { if !exact || len(path) == 0 {
return nil, fmt.Errorf("no exact AST node matching the specified range") return nil, fmt.Errorf("no exact AST node matching the specified range")
@ -47,26 +47,26 @@ func Format(ctx context.Context, f File, rng Range) ([]TextEdit, error) {
// of Go used to build the LSP server will determine how it formats code. // of Go used to build the LSP server will determine how it formats code.
// This should be acceptable for all users, who likely be prompted to rebuild // This should be acceptable for all users, who likely be prompted to rebuild
// the LSP server on each Go release. // the LSP server on each Go release.
fset := f.GetFileSet() fset := f.GetFileSet(ctx)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
if err := format.Node(buf, fset, node); err != nil { if err := format.Node(buf, fset, node); err != nil {
return nil, err return nil, err
} }
return computeTextEdits(f, buf.String()), nil return computeTextEdits(ctx, f, buf.String()), nil
} }
// Imports formats a file using the goimports tool. // Imports formats a file using the goimports tool.
func Imports(ctx context.Context, f File, rng Range) ([]TextEdit, error) { func Imports(ctx context.Context, f File, rng Range) ([]TextEdit, error) {
formatted, err := imports.Process(f.GetToken().Name(), f.GetContent(), nil) formatted, err := imports.Process(f.GetToken(ctx).Name(), f.GetContent(ctx), nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return computeTextEdits(f, string(formatted)), nil return computeTextEdits(ctx, f, string(formatted)), nil
} }
func computeTextEdits(file File, formatted string) (edits []TextEdit) { func computeTextEdits(ctx context.Context, file File, formatted string) (edits []TextEdit) {
u := strings.SplitAfter(string(file.GetContent()), "\n") u := strings.SplitAfter(string(file.GetContent(ctx)), "\n")
tok := file.GetToken() tok := file.GetToken(ctx)
f := strings.SplitAfter(formatted, "\n") f := strings.SplitAfter(formatted, "\n")
for _, op := range diff.Operations(u, f) { for _, op := range diff.Operations(u, f) {
start := lineStart(tok, op.I1+1) start := lineStart(tok, op.I1+1)

View File

@ -25,8 +25,8 @@ type ParameterInformation struct {
} }
func SignatureHelp(ctx context.Context, f File, pos token.Pos) (*SignatureInformation, error) { func SignatureHelp(ctx context.Context, f File, pos token.Pos) (*SignatureInformation, error) {
fAST := f.GetAST() fAST := f.GetAST(ctx)
pkg := f.GetPackage() pkg := f.GetPackage(ctx)
// Find a call expression surrounding the query position. // Find a call expression surrounding the query position.
var callExpr *ast.CallExpr var callExpr *ast.CallExpr

View File

@ -17,7 +17,7 @@ import (
// package does not directly access the file system. // package does not directly access the file system.
type View interface { type View interface {
GetFile(ctx context.Context, uri URI) (File, error) GetFile(ctx context.Context, uri URI) (File, error)
SetContent(ctx context.Context, uri URI, content []byte) (View, error) SetContent(ctx context.Context, uri URI, content []byte) error
GetAnalysisCache() *AnalysisCache GetAnalysisCache() *AnalysisCache
FileSet() *token.FileSet FileSet() *token.FileSet
} }
@ -27,11 +27,11 @@ type View interface {
// building blocks for most queries. Users of the source package can abstract // building blocks for most queries. Users of the source package can abstract
// the loading of packages into their own caching systems. // the loading of packages into their own caching systems.
type File interface { type File interface {
GetAST() *ast.File GetAST(ctx context.Context) *ast.File
GetFileSet() *token.FileSet GetFileSet(ctx context.Context) *token.FileSet
GetPackage() *packages.Package GetPackage(ctx context.Context) *packages.Package
GetToken() *token.File GetToken(ctx context.Context) *token.File
GetContent() []byte GetContent(ctx context.Context) []byte
} }
// Range represents a start and end position. // Range represents a start and end position.