internal/lsp: add structured layers to the cache

This is primarily to separate the levels because they have different cache
lifetimes and sharability.
This will allow us to share results between views and even between servers.

Change-Id: I280ca19d17a6ea8a15e48637d4445e2b6cf04769
Reviewed-on: https://go-review.googlesource.com/c/tools/+/177518
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
Ian Cottrell 2019-05-15 12:24:49 -04:00
parent abbb706b23
commit b9584148ef
31 changed files with 314 additions and 179 deletions

View File

@ -17,5 +17,5 @@ import (
)
func main() {
tool.Main(context.Background(), &cmd.Application{}, os.Args[1:])
tool.Main(context.Background(), cmd.New(nil), os.Args[1:])
}

24
internal/lsp/cache/cache.go vendored Normal file
View File

@ -0,0 +1,24 @@
// Copyright 2019 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 cache
import (
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/xlog"
)
func New() source.Cache {
return &cache{}
}
type cache struct {
}
func (c *cache) NewSession(log xlog.Logger) source.Session {
return &session{
cache: c,
log: log,
}
}

View File

@ -138,7 +138,7 @@ func (v *view) link(ctx context.Context, pkgPath string, pkg *packages.Package,
if f, _ := v.getFile(span.FileURI(filename)); f != nil {
gof, ok := f.(*goFile)
if !ok {
v.Logger().Errorf(ctx, "not a go file: %v", f.URI())
v.Session().Logger().Errorf(ctx, "not a go file: %v", f.URI())
continue
}
gof.meta = m
@ -270,23 +270,23 @@ func (v *view) cachePackage(ctx context.Context, pkg *pkg, meta *metadata) {
for _, file := range pkg.GetSyntax() {
// TODO: If a file is in multiple packages, which package do we store?
if !file.Pos().IsValid() {
v.Logger().Errorf(ctx, "invalid position for file %v", file.Name)
v.Session().Logger().Errorf(ctx, "invalid position for file %v", file.Name)
continue
}
tok := v.config.Fset.File(file.Pos())
if tok == nil {
v.Logger().Errorf(ctx, "no token.File for %v", file.Name)
v.Session().Logger().Errorf(ctx, "no token.File for %v", file.Name)
continue
}
fURI := span.FileURI(tok.Name())
f, err := v.getFile(fURI)
if err != nil {
v.Logger().Errorf(ctx, "no file: %v", err)
v.Session().Logger().Errorf(ctx, "no file: %v", err)
continue
}
gof, ok := f.(*goFile)
if !ok {
v.Logger().Errorf(ctx, "not a go file: %v", f.URI())
v.Session().Logger().Errorf(ctx, "not a go file: %v", f.URI())
continue
}
gof.token = tok

View File

@ -147,7 +147,7 @@ func (f *fileBase) read(ctx context.Context) {
// We don't know the content yet, so read it.
content, err := ioutil.ReadFile(f.filename())
if err != nil {
f.view.Logger().Errorf(ctx, "unable to read file %s: %v", f.filename(), err)
f.view.Session().Logger().Errorf(ctx, "unable to read file %s: %v", f.filename(), err)
return
}
f.content = content

View File

@ -152,7 +152,7 @@ func (v *view) fix(ctx context.Context, file *ast.File, tok *token.File, src []b
switch n := n.(type) {
case *ast.BadStmt:
if err := v.parseDeferOrGoStmt(n, parent, tok, src); err != nil {
v.log.Debugf(ctx, "unable to parse defer or go from *ast.BadStmt: %v", err)
v.Session().Logger().Debugf(ctx, "unable to parse defer or go from *ast.BadStmt: %v", err)
}
return false
default:

153
internal/lsp/cache/session.go vendored Normal file
View File

@ -0,0 +1,153 @@
// Copyright 2019 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 cache
import (
"context"
"fmt"
"strings"
"sync"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/xlog"
"golang.org/x/tools/internal/span"
)
type session struct {
cache *cache
// the logger to use to communicate back with the client
log xlog.Logger
viewMu sync.Mutex
views []*view
viewMap map[span.URI]source.View
}
func (s *session) Shutdown(ctx context.Context) {
s.viewMu.Lock()
defer s.viewMu.Unlock()
for _, view := range s.views {
view.shutdown(ctx)
}
s.views = nil
s.viewMap = nil
}
func (s *session) Cache() source.Cache {
return s.cache
}
func (s *session) NewView(name string, folder span.URI, config *packages.Config) source.View {
s.viewMu.Lock()
defer s.viewMu.Unlock()
ctx := context.Background()
backgroundCtx, cancel := context.WithCancel(ctx)
v := &view{
session: s,
baseCtx: ctx,
backgroundCtx: backgroundCtx,
builtinPkg: builtinPkg(*config),
cancel: cancel,
config: *config,
name: name,
folder: folder,
filesByURI: make(map[span.URI]viewFile),
filesByBase: make(map[string][]viewFile),
contentChanges: make(map[span.URI]func()),
mcache: &metadataCache{
packages: make(map[string]*metadata),
},
pcache: &packageCache{
packages: make(map[string]*entry),
},
}
s.views = append(s.views, v)
// we always need to drop the view map
s.viewMap = make(map[span.URI]source.View)
return v
}
// View returns the view by name.
func (s *session) View(name string) source.View {
s.viewMu.Lock()
defer s.viewMu.Unlock()
for _, view := range s.views {
if view.Name() == name {
return view
}
}
return nil
}
// ViewOf returns a view corresponding to the given URI.
// If the file is not already associated with a view, pick one using some heuristics.
func (s *session) ViewOf(uri span.URI) source.View {
s.viewMu.Lock()
defer s.viewMu.Unlock()
// check if we already know this file
if v, found := s.viewMap[uri]; found {
return v
}
// pick the best view for this file and memoize the result
v := s.bestView(uri)
s.viewMap[uri] = v
return v
}
func (s *session) Views() []source.View {
s.viewMu.Lock()
defer s.viewMu.Unlock()
result := make([]source.View, len(s.views))
for i, v := range s.views {
result[i] = v
}
return result
}
// bestView finds the best view to associate a given URI with.
// viewMu must be held when calling this method.
func (s *session) bestView(uri span.URI) source.View {
// we need to find the best view for this file
var longest source.View
for _, view := range s.views {
if longest != nil && len(longest.Folder()) > len(view.Folder()) {
continue
}
if strings.HasPrefix(string(uri), string(view.Folder())) {
longest = view
}
}
if longest != nil {
return longest
}
//TODO: are there any more heuristics we can use?
return s.views[0]
}
func (s *session) removeView(ctx context.Context, view *view) error {
s.viewMu.Lock()
defer s.viewMu.Unlock()
// we always need to drop the view map
s.viewMap = make(map[span.URI]source.View)
for i, v := range s.views {
if view == v {
// delete this view... we don't care about order but we do want to make
// sure we can garbage collect the view
s.views[i] = s.views[len(s.views)-1]
s.views[len(s.views)-1] = nil
s.views = s.views[:len(s.views)-1]
v.shutdown(ctx)
return nil
}
}
return fmt.Errorf("view %s for %v not found", view.Name(), view.Folder())
}
func (s *session) Logger() xlog.Logger {
return s.log
}

View File

@ -16,11 +16,12 @@ import (
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/xlog"
"golang.org/x/tools/internal/span"
)
type view struct {
session *session
// mu protects all mutable state of the view.
mu sync.Mutex
@ -36,9 +37,6 @@ type view struct {
// should be stopped.
cancel context.CancelFunc
// the logger to use to communicate back with the client
log xlog.Logger
// Name is the user visible name of this view.
name string
@ -94,28 +92,8 @@ type entry struct {
ready chan struct{} // closed to broadcast ready condition
}
func NewView(ctx context.Context, log xlog.Logger, name string, folder span.URI, config *packages.Config) source.View {
backgroundCtx, cancel := context.WithCancel(ctx)
v := &view{
baseCtx: ctx,
backgroundCtx: backgroundCtx,
builtinPkg: builtinPkg(*config),
cancel: cancel,
log: log,
config: *config,
name: name,
folder: folder,
filesByURI: make(map[span.URI]viewFile),
filesByBase: make(map[string][]viewFile),
contentChanges: make(map[span.URI]func()),
mcache: &metadataCache{
packages: make(map[string]*metadata),
},
pcache: &packageCache{
packages: make(map[string]*entry),
},
}
return v
func (v *view) Session() source.Session {
return v.session
}
// Name returns the user visible name of this view.
@ -138,7 +116,11 @@ func (v *view) SetEnv(env []string) {
v.config.Env = env
}
func (v *view) Shutdown(context.Context) {
func (v *view) Shutdown(ctx context.Context) {
v.session.removeView(ctx, v)
}
func (v *view) shutdown(context.Context) {
v.mu.Lock()
defer v.mu.Unlock()
if v.cancel != nil {
@ -393,7 +375,3 @@ func (v *view) mapFile(uri span.URI, f viewFile) {
v.filesByBase[basename] = append(v.filesByBase[basename], f)
}
}
func (v *view) Logger() xlog.Logger {
return v.log
}

View File

@ -24,7 +24,9 @@ import (
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/jsonrpc2"
"golang.org/x/tools/internal/lsp"
"golang.org/x/tools/internal/lsp/cache"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/tool"
)
@ -45,6 +47,9 @@ type Application struct {
// An initial, common go/packages configuration
Config packages.Config
// The base cache to use for sessions from this application.
Cache source.Cache
// Support for remote lsp server
Remote string `flag:"remote" help:"*EXPERIMENTAL* - forward all commands to a remote lsp"`
@ -52,6 +57,17 @@ type Application struct {
Verbose bool `flag:"v" help:"Verbose output"`
}
// Returns a new Application ready to run.
func New(config *packages.Config) *Application {
app := &Application{
Cache: cache.New(),
}
if config != nil {
app.Config = *config
}
return app
}
// Name implements tool.Application returning the binary name.
func (app *Application) Name() string { return "gopls" }
@ -135,7 +151,7 @@ func (app *Application) connect(ctx context.Context) (*connection, error) {
switch app.Remote {
case "":
connection := newConnection(app)
connection.Server = lsp.NewClientServer(connection.Client)
connection.Server = lsp.NewClientServer(app.Cache, connection.Client)
return connection, connection.initialize(ctx)
case "internal":
internalMu.Lock()
@ -150,7 +166,7 @@ func (app *Application) connect(ctx context.Context) (*connection, error) {
var jc *jsonrpc2.Conn
jc, connection.Server, _ = protocol.NewClient(jsonrpc2.NewHeaderStream(cr, cw), connection.Client)
go jc.Run(ctx)
go lsp.NewServer(jsonrpc2.NewHeaderStream(sr, sw)).Run(ctx)
go lsp.NewServer(app.Cache, jsonrpc2.NewHeaderStream(sr, sw)).Run(ctx)
if err := connection.initialize(ctx); err != nil {
return nil, err
}

View File

@ -37,9 +37,7 @@ func testCommandLine(t *testing.T, exporter packagestest.Exporter) {
r := &runner{
exporter: exporter,
data: data,
app: &cmd.Application{
Config: *data.Exported.Config,
},
app: cmd.New(data.Exported.Config),
}
tests.Run(t, r, data)
}

View File

@ -54,7 +54,7 @@ func TestDefinitionHelpExample(t *testing.T) {
fmt.Sprintf("%v:#%v", thisFile, cmd.ExampleOffset)} {
args := append(baseArgs, query)
got := captureStdOut(t, func() {
tool.Main(context.Background(), &cmd.Application{}, args)
tool.Main(context.Background(), cmd.New(nil), args)
})
if !expect.MatchString(got) {
t.Errorf("test with %v\nexpected:\n%s\ngot:\n%s", args, expect, got)

View File

@ -41,7 +41,7 @@ func (r *runner) Format(t *testing.T, data tests.Formats) {
//TODO: our error handling differs, for now just skip unformattable files
continue
}
app := &cmd.Application{}
app := cmd.New(nil)
app.Config = r.data.Config
got := captureStdOut(t, func() {
tool.Main(context.Background(), app, append([]string{"-remote=internal", "format"}, args...))

View File

@ -79,13 +79,13 @@ func (s *Serve) Run(ctx context.Context, args ...string) error {
go srv.Conn.Run(ctx)
}
if s.Address != "" {
return lsp.RunServerOnAddress(ctx, s.Address, run)
return lsp.RunServerOnAddress(ctx, s.app.Cache, s.Address, run)
}
if s.Port != 0 {
return lsp.RunServerOnPort(ctx, s.Port, run)
return lsp.RunServerOnPort(ctx, s.app.Cache, s.Port, run)
}
stream := jsonrpc2.NewHeaderStream(os.Stdin, os.Stdout)
srv := lsp.NewServer(stream)
srv := lsp.NewServer(s.app.Cache, stream)
srv.Conn.Logger = logger(s.Trace, out)
return srv.Conn.Run(ctx)
}

View File

@ -16,7 +16,7 @@ import (
func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
uri := span.NewURI(params.TextDocument.URI)
view := s.findView(ctx, uri)
view := s.session.ViewOf(uri)
_, m, err := getSourceFile(ctx, view, uri)
if err != nil {
return nil, err

View File

@ -17,7 +17,7 @@ import (
func (s *Server) completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) {
uri := span.NewURI(params.TextDocument.URI)
view := s.findView(ctx, uri)
view := s.session.ViewOf(uri)
f, m, err := getGoFile(ctx, view, uri)
if err != nil {
return nil, err
@ -32,19 +32,19 @@ func (s *Server) completion(ctx context.Context, params *protocol.CompletionPara
}
items, prefix, err := source.Completion(ctx, f, rng.Start)
if err != nil {
s.log.Infof(ctx, "no completions found for %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
s.session.Logger().Infof(ctx, "no completions found for %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
}
// We might need to adjust the position to account for the prefix.
pos := params.Position
if prefix.Pos().IsValid() {
spn, err := span.NewRange(view.FileSet(), prefix.Pos(), 0).Span()
if err != nil {
s.log.Infof(ctx, "failed to get span for prefix position: %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
s.session.Logger().Infof(ctx, "failed to get span for prefix position: %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
}
if prefixPos, err := m.Position(spn.Start()); err == nil {
pos = prefixPos
} else {
s.log.Infof(ctx, "failed to convert prefix position: %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
s.session.Logger().Infof(ctx, "failed to convert prefix position: %s:%v:%v: %v", uri, int(params.Position.Line), int(params.Position.Character), err)
}
}
return &protocol.CompletionList{

View File

@ -14,7 +14,7 @@ import (
func (s *Server) definition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) {
uri := span.NewURI(params.TextDocument.URI)
view := s.findView(ctx, uri)
view := s.session.ViewOf(uri)
f, m, err := getGoFile(ctx, view, uri)
if err != nil {
return nil, err
@ -48,7 +48,7 @@ func (s *Server) definition(ctx context.Context, params *protocol.TextDocumentPo
func (s *Server) typeDefinition(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.Location, error) {
uri := span.NewURI(params.TextDocument.URI)
view := s.findView(ctx, uri)
view := s.session.ViewOf(uri)
f, m, err := getGoFile(ctx, view, uri)
if err != nil {
return nil, err

View File

@ -14,12 +14,12 @@ import (
func (s *Server) Diagnostics(ctx context.Context, view source.View, uri span.URI) {
if ctx.Err() != nil {
s.log.Errorf(ctx, "canceling diagnostics for %s: %v", uri, ctx.Err())
s.session.Logger().Errorf(ctx, "canceling diagnostics for %s: %v", uri, ctx.Err())
return
}
reports, err := source.Diagnostics(ctx, view, uri)
if err != nil {
s.log.Errorf(ctx, "failed to compute diagnostics for %s: %v", uri, err)
s.session.Logger().Errorf(ctx, "failed to compute diagnostics for %s: %v", uri, err)
return
}

View File

@ -15,7 +15,7 @@ import (
func (s *Server) formatting(ctx context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) {
uri := span.NewURI(params.TextDocument.URI)
view := s.findView(ctx, uri)
view := s.session.ViewOf(uri)
spn := span.New(uri, span.Point{}, span.Point{})
return formatRange(ctx, view, spn)
}

View File

@ -125,7 +125,7 @@ func (s *Server) initialized(ctx context.Context, params *protocol.InitializedPa
}},
})
}
for _, view := range s.views {
for _, view := range s.session.Views() {
config, err := s.client.Configuration(ctx, &protocol.ConfigurationParams{
Items: []protocol.ConfigurationItem{{
ScopeURI: protocol.NewURI(view.Folder()),
@ -142,7 +142,7 @@ func (s *Server) initialized(ctx context.Context, params *protocol.InitializedPa
}
buf := &bytes.Buffer{}
PrintVersionInfo(buf, true, false)
s.log.Infof(ctx, "%s", buf)
s.session.Logger().Infof(ctx, "%s", buf)
return nil
}
@ -195,13 +195,7 @@ func (s *Server) shutdown(ctx context.Context) error {
return jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidRequest, "server not initialized")
}
// drop all the active views
s.viewMu.Lock()
defer s.viewMu.Unlock()
for _, v := range s.views {
v.Shutdown(ctx)
}
s.views = nil
s.viewMap = nil
s.session.Shutdown(ctx)
s.isInitialized = false
return nil
}

View File

@ -14,7 +14,7 @@ import (
func (s *Server) documentHighlight(ctx context.Context, params *protocol.TextDocumentPositionParams) ([]protocol.DocumentHighlight, error) {
uri := span.NewURI(params.TextDocument.URI)
view := s.findView(ctx, uri)
view := s.session.ViewOf(uri)
f, m, err := getGoFile(ctx, view, uri)
if err != nil {
return nil, err

View File

@ -15,7 +15,7 @@ import (
func (s *Server) hover(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.Hover, error) {
uri := span.NewURI(params.TextDocument.URI)
view := s.findView(ctx, uri)
view := s.session.ViewOf(uri)
f, m, err := getGoFile(ctx, view, uri)
if err != nil {
return nil, err

View File

@ -14,7 +14,7 @@ import (
func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) {
uri := span.NewURI(params.TextDocument.URI)
view := s.findView(ctx, uri)
view := s.session.ViewOf(uri)
f, m, err := getGoFile(ctx, view, uri)
if err != nil {
return nil, err

View File

@ -33,19 +33,20 @@ type runner struct {
data *tests.Data
}
func testLSP(t *testing.T, exporter packagestest.Exporter) {
ctx := context.Background()
const viewName = "lsp_test"
func testLSP(t *testing.T, exporter packagestest.Exporter) {
data := tests.Load(t, exporter, "testdata")
defer data.Exported.Cleanup()
log := xlog.New(xlog.StdSink{})
cache := cache.New()
session := cache.NewSession(log)
session.NewView(viewName, span.FileURI(data.Config.Dir), &data.Config)
r := &runner{
server: &Server{
views: []source.View{cache.NewView(ctx, log, "lsp_test", span.FileURI(data.Config.Dir), &data.Config)},
viewMap: make(map[span.URI]source.View),
session: session,
undelivered: make(map[span.URI][]source.Diagnostic),
log: log,
},
data: data,
}
@ -53,7 +54,7 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) {
}
func (r *runner) Diagnostics(t *testing.T, data tests.Diagnostics) {
v := r.server.views[0]
v := r.server.session.View(viewName)
for uri, want := range data {
results, err := source.Diagnostics(context.Background(), v, uri)
if err != nil {
@ -293,7 +294,7 @@ func (r *runner) Format(t *testing.T, data tests.Formats) {
}
continue
}
_, m, err := getSourceFile(ctx, r.server.findView(ctx, uri), uri)
_, m, err := getSourceFile(ctx, r.server.session.ViewOf(uri), uri)
if err != nil {
t.Error(err)
}

View File

@ -18,30 +18,32 @@ import (
)
// NewClientServer
func NewClientServer(client protocol.Client) *Server {
func NewClientServer(cache source.Cache, client protocol.Client) *Server {
return &Server{
client: client,
log: xlog.New(protocol.NewLogger(client)),
client: client,
session: cache.NewSession(xlog.New(protocol.NewLogger(client))),
}
}
// NewServer starts an LSP server on the supplied stream, and waits until the
// stream is closed.
func NewServer(stream jsonrpc2.Stream) *Server {
func NewServer(cache source.Cache, stream jsonrpc2.Stream) *Server {
s := &Server{}
s.Conn, s.client, s.log = protocol.NewServer(stream, s)
var log xlog.Logger
s.Conn, s.client, log = protocol.NewServer(stream, s)
s.session = cache.NewSession(log)
return s
}
// RunServerOnPort starts an LSP server on the given port and does not exit.
// This function exists for debugging purposes.
func RunServerOnPort(ctx context.Context, port int, h func(s *Server)) error {
return RunServerOnAddress(ctx, fmt.Sprintf(":%v", port), h)
func RunServerOnPort(ctx context.Context, cache source.Cache, port int, h func(s *Server)) error {
return RunServerOnAddress(ctx, cache, fmt.Sprintf(":%v", port), h)
}
// RunServerOnPort starts an LSP server on the given port and does not exit.
// This function exists for debugging purposes.
func RunServerOnAddress(ctx context.Context, addr string, h func(s *Server)) error {
func RunServerOnAddress(ctx context.Context, cache source.Cache, addr string, h func(s *Server)) error {
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
@ -52,7 +54,7 @@ func RunServerOnAddress(ctx context.Context, addr string, h func(s *Server)) err
return err
}
stream := jsonrpc2.NewHeaderStream(conn, conn)
s := NewServer(stream)
s := NewServer(cache, stream)
h(s)
go s.Run(ctx)
}
@ -65,7 +67,6 @@ func (s *Server) Run(ctx context.Context) error {
type Server struct {
Conn *jsonrpc2.Conn
client protocol.Client
log xlog.Logger
initializedMu sync.Mutex
isInitialized bool // set once the server has received "initialize" request
@ -81,9 +82,7 @@ type Server struct {
textDocumentSyncKind protocol.TextDocumentSyncKind
viewMu sync.Mutex
views []source.View
viewMap map[span.URI]source.View
session source.Session
// undelivered is a cache of any diagnostics that the server
// failed to deliver for some reason.

View File

@ -14,7 +14,7 @@ import (
func (s *Server) signatureHelp(ctx context.Context, params *protocol.TextDocumentPositionParams) (*protocol.SignatureHelp, error) {
uri := span.NewURI(params.TextDocument.URI)
view := s.findView(ctx, uri)
view := s.session.ViewOf(uri)
f, m, err := getGoFile(ctx, view, uri)
if err != nil {
return nil, err
@ -29,7 +29,7 @@ func (s *Server) signatureHelp(ctx context.Context, params *protocol.TextDocumen
}
info, err := source.SignatureHelp(ctx, f, rng.Start)
if err != nil {
s.log.Infof(ctx, "no signature help for %s:%v:%v : %s", uri, int(params.Position.Line), int(params.Position.Character), err)
s.session.Logger().Infof(ctx, "no signature help for %s:%v:%v : %s", uri, int(params.Position.Line), int(params.Position.Character), err)
}
return toProtocolSignatureHelp(info), nil
}

View File

@ -163,7 +163,7 @@ func formatFieldList(ctx context.Context, v View, list *ast.FieldList) ([]string
cfg := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 4}
b := &bytes.Buffer{}
if err := cfg.Fprint(b, v.FileSet(), p.Type); err != nil {
v.Logger().Errorf(ctx, "unable to print type %v", p.Type)
v.Session().Logger().Errorf(ctx, "unable to print type %v", p.Type)
continue
}
typ := replacer.Replace(b.String())

View File

@ -157,29 +157,29 @@ func pointToSpan(ctx context.Context, v View, spn span.Span) span.Span {
// Don't set a range if it's anything other than a type error.
f, err := v.GetFile(ctx, spn.URI())
if err != nil {
v.Logger().Errorf(ctx, "Could find file for diagnostic: %v", spn.URI())
v.Session().Logger().Errorf(ctx, "Could find file for diagnostic: %v", spn.URI())
return spn
}
diagFile, ok := f.(GoFile)
if !ok {
v.Logger().Errorf(ctx, "Not a go file: %v", spn.URI())
v.Session().Logger().Errorf(ctx, "Not a go file: %v", spn.URI())
return spn
}
tok := diagFile.GetToken(ctx)
if tok == nil {
v.Logger().Errorf(ctx, "Could not find tokens for diagnostic: %v", spn.URI())
v.Session().Logger().Errorf(ctx, "Could not find tokens for diagnostic: %v", spn.URI())
return spn
}
content := diagFile.GetContent(ctx)
if content == nil {
v.Logger().Errorf(ctx, "Could not find content for diagnostic: %v", spn.URI())
v.Session().Logger().Errorf(ctx, "Could not find content for diagnostic: %v", spn.URI())
return spn
}
c := span.NewTokenConverter(diagFile.GetFileSet(ctx), tok)
s, err := spn.WithOffset(c)
//we just don't bother producing an error if this failed
if err != nil {
v.Logger().Errorf(ctx, "invalid span for diagnostic: %v: %v", spn.URI(), err)
v.Session().Logger().Errorf(ctx, "invalid span for diagnostic: %v: %v", spn.URI(), err)
return spn
}
start := s.Start()

View File

@ -32,14 +32,14 @@ type runner struct {
}
func testSource(t *testing.T, exporter packagestest.Exporter) {
ctx := context.Background()
data := tests.Load(t, exporter, "../testdata")
defer data.Exported.Cleanup()
log := xlog.New(xlog.StdSink{})
cache := cache.New()
session := cache.NewSession(log)
r := &runner{
view: cache.NewView(ctx, log, "source_test", span.FileURI(data.Config.Dir), &data.Config),
view: session.NewView("source_test", span.FileURI(data.Config.Dir), &data.Config),
data: data,
}
tests.Run(t, r, data)

View File

@ -18,13 +18,47 @@ import (
"golang.org/x/tools/internal/span"
)
// View abstracts the underlying architecture of the package using the source
// package. The view provides access to files and their contents, so the source
// Cache abstracts the core logic of dealing with the environment from the
// higher level logic that processes the information to produce results.
// The cache provides access to files and their contents, so the source
// package does not directly access the file system.
// A single cache is intended to be process wide, and is the primary point of
// sharing between all consumers.
// A cache may have many active sessions at any given time.
type Cache interface {
// NewSession creates a new Session manager and returns it.
NewSession(log xlog.Logger) Session
}
// Session represents a single connection from a client.
// This is the level at which things like open files are maintained on behalf
// of the client.
// A session may have many active views at any given time.
type Session interface {
// NewView creates a new View and returns it.
NewView(name string, folder span.URI, config *packages.Config) View
// Cache returns the cache that created this session.
Cache() Cache
// Returns the logger in use for this session.
Logger() xlog.Logger
View(name string) View
ViewOf(uri span.URI) View
Views() []View
Shutdown(ctx context.Context)
}
// View represents a single workspace.
// This is the level at which we maintain configuration like working directory
// and build tags.
type View interface {
// Session returns the session that created this view.
Session() Session
Name() string
Folder() span.URI
Logger() xlog.Logger
FileSet() *token.FileSet
BuiltinPackage() *ast.Package
GetFile(ctx context.Context, uri span.URI) (File, error)

View File

@ -14,7 +14,7 @@ import (
func (s *Server) documentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]protocol.DocumentSymbol, error) {
uri := span.NewURI(params.TextDocument.URI)
view := s.findView(ctx, uri)
view := s.session.ViewOf(uri)
f, m, err := getGoFile(ctx, view, uri)
if err != nil {
return nil, err

View File

@ -42,7 +42,7 @@ func (s *Server) didChange(ctx context.Context, params *protocol.DidChangeTextDo
}
func (s *Server) cacheAndDiagnose(ctx context.Context, uri span.URI, content []byte) error {
view := s.findView(ctx, uri)
view := s.session.ViewOf(uri)
if err := view.SetContent(ctx, uri, content); err != nil {
return err
}
@ -64,7 +64,7 @@ func (s *Server) applyChanges(ctx context.Context, params *protocol.DidChangeTex
}
uri := span.NewURI(params.TextDocument.URI)
view := s.findView(ctx, uri)
view := s.session.ViewOf(uri)
file, m, err := getSourceFile(ctx, view, uri)
if err != nil {
return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found")
@ -97,6 +97,6 @@ func (s *Server) didSave(ctx context.Context, params *protocol.DidSaveTextDocume
func (s *Server) didClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
uri := span.NewURI(params.TextDocument.URI)
view := s.findView(ctx, uri)
view := s.session.ViewOf(uri)
return view.SetContent(ctx, uri, nil)
}

View File

@ -11,20 +11,19 @@ import (
"go/parser"
"go/token"
"os"
"strings"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/lsp/cache"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
)
func (s *Server) changeFolders(ctx context.Context, event protocol.WorkspaceFoldersChangeEvent) error {
s.log.Infof(ctx, "change folders")
for _, folder := range event.Removed {
if err := s.removeView(ctx, folder.Name, span.NewURI(folder.URI)); err != nil {
return err
view := s.session.View(folder.Name)
if view != nil {
view.Shutdown(ctx)
} else {
return fmt.Errorf("view %s for %v not found", folder.Name, folder.URI)
}
}
@ -37,17 +36,14 @@ func (s *Server) changeFolders(ctx context.Context, event protocol.WorkspaceFold
}
func (s *Server) addView(ctx context.Context, name string, uri span.URI) error {
s.viewMu.Lock()
defer s.viewMu.Unlock()
// We need a "detached" context so it does not get timeout cancelled.
// TODO(iancottrell): Do we need to copy any values across?
viewContext := context.Background()
s.log.Infof(viewContext, "add view %v as %v", name, uri)
folderPath, err := uri.Filename()
if err != nil {
return err
}
s.views = append(s.views, cache.NewView(viewContext, s.log, name, uri, &packages.Config{
s.session.NewView(name, uri, &packages.Config{
Context: viewContext,
Dir: folderPath,
Env: os.Environ(),
@ -58,65 +54,7 @@ func (s *Server) addView(ctx context.Context, name string, uri span.URI) error {
return parser.ParseFile(fset, filename, src, parser.AllErrors|parser.ParseComments)
},
Tests: true,
}))
// we always need to drop the view map
s.viewMap = make(map[span.URI]source.View)
})
return nil
}
func (s *Server) removeView(ctx context.Context, name string, uri span.URI) error {
s.viewMu.Lock()
defer s.viewMu.Unlock()
// we always need to drop the view map
s.viewMap = make(map[span.URI]source.View)
s.log.Infof(ctx, "drop view %v as %v", name, uri)
for i, view := range s.views {
if view.Name() == name {
// delete this view... we don't care about order but we do want to make
// sure we can garbage collect the view
s.views[i] = s.views[len(s.views)-1]
s.views[len(s.views)-1] = nil
s.views = s.views[:len(s.views)-1]
view.Shutdown(ctx)
return nil
}
}
return fmt.Errorf("view %s for %v not found", name, uri)
}
// findView returns the view corresponding to the given URI.
// If the file is not already associated with a view, pick one using some heuristics.
func (s *Server) findView(ctx context.Context, uri span.URI) source.View {
s.viewMu.Lock()
defer s.viewMu.Unlock()
// check if we already know this file
if v, found := s.viewMap[uri]; found {
return v
}
// pick the best view for this file and memoize the result
v := s.bestView(ctx, uri)
s.viewMap[uri] = v
return v
}
// bestView finds the best view to associate a given URI with.
// viewMu must be held when calling this method.
func (s *Server) bestView(ctx context.Context, uri span.URI) source.View {
// we need to find the best view for this file
var longest source.View
for _, view := range s.views {
if longest != nil && len(longest.Folder()) > len(view.Folder()) {
continue
}
if strings.HasPrefix(string(uri), string(view.Folder())) {
longest = view
}
}
if longest != nil {
return longest
}
//TODO: are there any more heuristics we can use?
return s.views[0]
}