1
0
mirror of https://github.com/golang/go synced 2024-11-18 16:14:46 -07:00

internal/lsp: move DidModifyFile into internal/lsp/cache

This change is the next step in unification of text synchronization
methods. The logic really belongs in the internal/lsp/cache package
rather than the internal/lsp package.

Pulled out a function to run diagnostics on a file (diagnostics are still
run async).

Change-Id: I5e237411a02af210ad386b37a6c2aa62ef723567
Reviewed-on: https://go-review.googlesource.com/c/tools/+/210784
Reviewed-by: Heschi Kreinick <heschi@google.com>
This commit is contained in:
Rebecca Stambler 2019-12-10 12:29:37 -05:00
parent dfebd2802a
commit 4403f79810
8 changed files with 134 additions and 106 deletions

View File

@ -31,7 +31,7 @@ type session struct {
viewMu sync.Mutex
views []*view
viewMap map[span.URI]source.View
viewMap map[span.URI]*view
overlayMu sync.Mutex
overlays map[span.URI]*overlay
@ -85,7 +85,7 @@ func (s *session) NewView(ctx context.Context, name string, folder span.URI, opt
}
s.views = append(s.views, v)
// we always need to drop the view map
s.viewMap = make(map[span.URI]source.View)
s.viewMap = make(map[span.URI]*view)
return v, snapshot, nil
}
@ -167,6 +167,10 @@ func (s *session) View(name string) source.View {
// 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, error) {
return s.viewOf(uri)
}
func (s *session) viewOf(uri span.URI) (*view, error) {
s.viewMu.Lock()
defer s.viewMu.Unlock()
@ -208,12 +212,12 @@ func (s *session) Views() []source.View {
// 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, error) {
func (s *session) bestView(uri span.URI) (*view, error) {
if len(s.views) == 0 {
return nil, errors.Errorf("no views in the session")
}
// we need to find the best view for this file
var longest source.View
var longest *view
for _, view := range s.views {
if longest != nil && len(longest.Folder()) > len(view.Folder()) {
continue
@ -265,22 +269,51 @@ func (s *session) updateView(ctx context.Context, view *view, options source.Opt
return v, snapshot, nil
}
func (s *session) dropView(ctx context.Context, view *view) (int, error) {
func (s *session) dropView(ctx context.Context, v *view) (int, error) {
// 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 {
s.viewMap = make(map[span.URI]*view)
for i := range s.views {
if v == s.views[i] {
// we found the view, drop it and return the index it was found at
s.views[i] = nil
v.shutdown(ctx)
return i, nil
}
}
return -1, errors.Errorf("view %s for %v not found", view.Name(), view.Folder())
return -1, errors.Errorf("view %s for %v not found", v.Name(), v.Folder())
}
func (s *session) DidModifyFile(ctx context.Context, c source.FileModification) error {
ctx = telemetry.URI.With(ctx, c.URI)
for _, view := range s.viewsOf(c.URI) {
switch c.Action {
case source.Open:
kind := source.DetectLanguage(c.LanguageID, c.URI.Filename())
if kind == source.UnknownKind {
return errors.Errorf("DidModifyFile: unknown file kind for %s", c.URI)
}
return s.didOpen(ctx, c.URI, kind, c.Version, c.Text)
case source.Save:
s.didSave(c.URI, c.Version)
case source.Close:
s.didClose(c.URI)
}
// Set the content for the file, only for didChange and didClose events.
switch c.Action {
case source.Change, source.Close:
f, err := view.GetFile(ctx, c.URI)
if err != nil {
return err
}
view.setContent(ctx, f, c.Version, c.Text)
}
}
return nil
}
// TODO: Propagate the language ID through to the view.
func (s *session) DidOpen(ctx context.Context, uri span.URI, kind source.FileKind, version float64, text []byte) error {
func (s *session) didOpen(ctx context.Context, uri span.URI, kind source.FileKind, version float64, text []byte) error {
ctx = telemetry.File.With(ctx, uri)
// Files with _ prefixes are ignored.
@ -311,7 +344,7 @@ func (s *session) DidOpen(ctx context.Context, uri span.URI, kind source.FileKin
return nil
}
func (s *session) DidSave(uri span.URI, version float64) {
func (s *session) didSave(uri span.URI, version float64) {
s.overlayMu.Lock()
defer s.overlayMu.Unlock()
@ -321,7 +354,7 @@ func (s *session) DidSave(uri span.URI, version float64) {
}
}
func (s *session) DidClose(uri span.URI) {
func (s *session) didClose(uri span.URI) {
s.openFiles.Delete(uri)
}
@ -338,22 +371,22 @@ func (s *session) GetFile(uri span.URI, kind source.FileKind) source.FileHandle
return s.cache.GetFile(uri, kind)
}
func (s *session) SetOverlay(uri span.URI, kind source.FileKind, version float64, data []byte) {
func (s *session) setOverlay(f source.File, version float64, data []byte) {
s.overlayMu.Lock()
defer func() {
s.overlayMu.Unlock()
s.filesWatchMap.Notify(uri, source.Change)
s.filesWatchMap.Notify(f.URI(), source.Change)
}()
if data == nil {
delete(s.overlays, uri)
delete(s.overlays, f.URI())
return
}
s.overlays[uri] = &overlay{
s.overlays[f.URI()] = &overlay{
session: s,
uri: uri,
kind: kind,
uri: f.URI(),
kind: f.Kind(),
data: data,
hash: hashContents(data),
version: version,

View File

@ -323,12 +323,12 @@ func (v *view) getSnapshot() *snapshot {
return v.snapshot
}
// SetContent sets the overlay contents for a file.
func (v *view) SetContent(ctx context.Context, uri span.URI, version float64, content []byte) {
// setContent sets the overlay contents for a file.
func (v *view) setContent(ctx context.Context, f source.File, version float64, content []byte) {
v.mu.Lock()
defer v.mu.Unlock()
if v.Ignore(uri) {
if v.Ignore(f.URI()) {
return
}
@ -337,8 +337,7 @@ func (v *view) SetContent(ctx context.Context, uri span.URI, version float64, co
v.cancel()
v.backgroundCtx, v.cancel = context.WithCancel(v.baseCtx)
kind := source.DetectLanguage("", uri.Filename())
v.session.SetOverlay(uri, kind, version, content)
v.session.setOverlay(f, version, content)
}
// FindFile returns the file if the given URI is already a part of the view.

View File

@ -11,11 +11,20 @@ import (
"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/span"
"golang.org/x/tools/internal/telemetry/log"
"golang.org/x/tools/internal/telemetry/trace"
)
func (s *Server) diagnose(snapshot source.Snapshot, f source.File) error {
switch f.Kind() {
case source.Go:
go s.diagnoseFile(snapshot, f)
case source.Mod:
go s.diagnoseSnapshot(snapshot)
}
return nil
}
func (s *Server) diagnoseSnapshot(snapshot source.Snapshot) {
ctx := snapshot.View().BackgroundContext()
ctx, done := trace.StartSpan(ctx, "lsp:background-worker")
@ -50,18 +59,13 @@ func (s *Server) diagnoseSnapshot(snapshot source.Snapshot) {
}
}
func (s *Server) diagnoseFile(snapshot source.Snapshot, uri span.URI) {
func (s *Server) diagnoseFile(snapshot source.Snapshot, f source.File) {
ctx := snapshot.View().BackgroundContext()
ctx, done := trace.StartSpan(ctx, "lsp:background-worker")
defer done()
ctx = telemetry.File.With(ctx, uri)
ctx = telemetry.File.With(ctx, f.URI())
f, err := snapshot.View().GetFile(ctx, uri)
if err != nil {
log.Error(ctx, "diagnoseFile: no file", err)
return
}
reports, warningMsg, err := source.Diagnostics(ctx, snapshot, f, true, snapshot.View().Options().DisabledAnalyses)
// Check the warning message first.
if warningMsg != "" {

View File

@ -57,7 +57,19 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) {
t.Fatal(err)
}
for filename, content := range data.Config.Overlay {
session.SetOverlay(span.FileURI(filename), source.DetectLanguage("", filename), -1, content)
kind := source.DetectLanguage("", filename)
if kind != source.Go {
continue
}
if err := session.DidModifyFile(ctx, source.FileModification{
URI: span.FileURI(filename),
Action: source.Open,
Version: -1,
Text: content,
LanguageID: "go",
}); err != nil {
t.Fatal(err)
}
}
r := &runner{
server: &Server{

View File

@ -61,7 +61,19 @@ func testSource(t *testing.T, exporter packagestest.Exporter) {
ctx: ctx,
}
for filename, content := range data.Config.Overlay {
session.SetOverlay(span.FileURI(filename), source.DetectLanguage("", filename), -1, content)
kind := source.DetectLanguage("", filename)
if kind != source.Go {
continue
}
if err := session.DidModifyFile(ctx, source.FileModification{
URI: span.FileURI(filename),
Action: source.Open,
Version: -1,
Text: content,
LanguageID: "go",
}); err != nil {
t.Fatal(err)
}
}
tests.Run(t, r, data)
}

View File

@ -99,9 +99,6 @@ type View interface {
// already part of the view.
FindFile(ctx context.Context, uri span.URI) File
// Called to set the effective contents of a file from this view.
SetContent(ctx context.Context, uri span.URI, version float64, content []byte)
// BackgroundContext returns a context used for all background processing
// on behalf of this view.
BackgroundContext() context.Context
@ -167,20 +164,11 @@ type Session interface {
// content from the underlying cache if no overlay is present.
FileSystem
// DidOpen is invoked each time a file is opened in the editor.
DidOpen(ctx context.Context, uri span.URI, kind FileKind, version float64, text []byte) error
// DidSave is invoked each time an open file is saved in the editor.
DidSave(uri span.URI, version float64)
// DidClose is invoked each time an open file is closed in the editor.
DidClose(uri span.URI)
// IsOpen returns whether the editor currently has a file open.
IsOpen(uri span.URI) bool
// Called to set the effective contents of a file from this session.
SetOverlay(uri span.URI, kind FileKind, version float64, data []byte)
// DidModifyFile reports a file modification to the session.
DidModifyFile(ctx context.Context, c FileModification) error
// DidChangeOutOfBand is called when a file under the root folder changes.
// If the file was open in the editor, it returns true.

View File

@ -12,7 +12,6 @@ import (
"golang.org/x/tools/internal/jsonrpc2"
"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/span"
errors "golang.org/x/xerrors"
)
@ -20,13 +19,25 @@ import (
func (s *Server) didOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error {
// Confirm that the file's language ID is related to Go.
uri := span.NewURI(params.TextDocument.URI)
return s.didModifyFile(ctx, source.FileModification{
if err := s.session.DidModifyFile(ctx, source.FileModification{
URI: uri,
Action: source.Open,
Version: params.TextDocument.Version,
Text: []byte(params.TextDocument.Text),
LanguageID: params.TextDocument.LanguageID,
})
}); err != nil {
return err
}
view, err := s.session.ViewOf(uri)
if err != nil {
return err
}
f, err := view.GetFile(ctx, uri)
if err != nil {
return err
}
// Always run diagnostics when a file is opened.
return s.diagnose(view.Snapshot(), f)
}
func (s *Server) didSave(ctx context.Context, params *protocol.DidSaveTextDocumentParams) error {
@ -38,11 +49,11 @@ func (s *Server) didSave(ctx context.Context, params *protocol.DidSaveTextDocume
if params.Text != nil {
c.Text = []byte(*params.Text)
}
return s.didModifyFile(ctx, c)
return s.session.DidModifyFile(ctx, c)
}
func (s *Server) didClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
return s.didModifyFile(ctx, source.FileModification{
return s.session.DidModifyFile(ctx, source.FileModification{
URI: span.NewURI(params.TextDocument.URI),
Action: source.Close,
Version: -1,
@ -56,67 +67,32 @@ func (s *Server) didChange(ctx context.Context, params *protocol.DidChangeTextDo
if err != nil {
return err
}
return s.didModifyFile(ctx, source.FileModification{
if err := s.session.DidModifyFile(ctx, source.FileModification{
URI: uri,
Action: source.Change,
Version: params.TextDocument.Version,
Text: text,
})
}
// didModifyFile propagates the information about the file modification
// to the cache layer and runs diagnostics.
//
// TODO(rstambler): This function should be mostly unnecessary once we unify the methods
// for making changes to a file in internal/lsp/cache.
func (s *Server) didModifyFile(ctx context.Context, c source.FileModification) error {
ctx = telemetry.URI.With(ctx, c.URI)
view, err := s.session.ViewOf(c.URI)
}); err != nil {
return err
}
view, err := s.session.ViewOf(uri)
if err != nil {
return err
}
switch c.Action {
case source.Open:
kind := source.DetectLanguage(c.LanguageID, c.URI.Filename())
if kind == source.UnknownKind {
return errors.Errorf("didModifyFile: unknown file kind for %s", c.URI)
}
if err := s.session.DidOpen(ctx, c.URI, kind, c.Version, c.Text); err != nil {
return err
}
case source.Change:
view.SetContent(ctx, c.URI, c.Version, c.Text)
// Ideally, we should be able to specify that a generated file should be opened as read-only.
// Tell the user that they should not be editing a generated file.
if s.wasFirstChange(c.URI) && source.IsGenerated(ctx, view, c.URI) {
s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
Message: fmt.Sprintf("Do not edit this file! %s is a generated file.", c.URI.Filename()),
Type: protocol.Warning,
})
}
case source.Save:
s.session.DidSave(c.URI, c.Version)
case source.Close:
s.session.DidClose(c.URI)
view.SetContent(ctx, c.URI, c.Version, c.Text)
// Ideally, we should be able to specify that a generated file should be opened as read-only.
// Tell the user that they should not be editing a generated file.
if s.wasFirstChange(uri) && source.IsGenerated(ctx, view, uri) {
s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
Message: fmt.Sprintf("Do not edit this file! %s is a generated file.", uri.Filename()),
Type: protocol.Warning,
})
}
// We should run diagnostics after opening or changing a file.
switch c.Action {
case source.Open, source.Change:
f, err := view.GetFile(ctx, c.URI)
if err != nil {
return err
}
if f.Kind() == source.Mod {
go s.diagnoseSnapshot(view.Snapshot())
} else {
go s.diagnoseFile(view.Snapshot(), c.URI)
}
f, err := view.GetFile(ctx, uri)
if err != nil {
return err
}
return nil
// Always update diagnostics after a file change.
return s.diagnose(view.Snapshot(), f)
}
func (s *Server) wasFirstChange(uri span.URI) bool {

View File

@ -34,7 +34,11 @@ func (s *Server) didChangeWatchedFiles(ctx context.Context, params *protocol.Did
if s.session.DidChangeOutOfBand(ctx, uri, action) {
// If we had been tracking the given file,
// recompute diagnostics to reflect updated file contents.
go s.diagnoseFile(view.Snapshot(), uri)
f, err := view.GetFile(ctx, uri)
if err != nil {
return err
}
return s.diagnose(view.Snapshot(), f)
}
case source.Delete:
f := view.FindFile(ctx, uri)
@ -82,7 +86,7 @@ func (s *Server) didChangeWatchedFiles(ctx context.Context, params *protocol.Did
}
// Refresh diagnostics for the package the file belonged to.
go s.diagnoseFile(view.Snapshot(), otherFile.URI())
go s.diagnoseFile(view.Snapshot(), otherFile)
}
}
}