mirror of
https://github.com/golang/go
synced 2024-11-18 23:05:06 -07:00
a7dab0268b
This change moves to our ultimate approach of diagnostics the snapshot on every file change, instead of carefully picking which files and packages to diagnose. Analyses are shown for packages whose files are open in the editor. Reverse dependencies are no longer needed for source.Diagnostics because they will be invalidated when the snapshot is cloned, so diagnosing the entire snapshot will bring them up to date. This even works for go.mod files because all of workspace-level `go list`s will be canceled as the user types, and then we trigger an uncancellable go/packages.Load when the user saves. There is still room for improvement here, but it will require much more careful invalidation of metadata for go.mod files. Change-Id: Id068505634b5e701c6f861a61b09a4c6704c565f Reviewed-on: https://go-review.googlesource.com/c/tools/+/214419 Run-TryBot: Rebecca Stambler <rstambler@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Heschi Kreinick <heschi@google.com>
225 lines
6.6 KiB
Go
225 lines
6.6 KiB
Go
// 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 lsp
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
|
|
"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"
|
|
)
|
|
|
|
func (s *Server) didOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error {
|
|
_, err := s.didModifyFile(ctx, source.FileModification{
|
|
URI: span.NewURI(params.TextDocument.URI),
|
|
Action: source.Open,
|
|
Version: params.TextDocument.Version,
|
|
Text: []byte(params.TextDocument.Text),
|
|
LanguageID: params.TextDocument.LanguageID,
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (s *Server) didChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error {
|
|
uri := span.NewURI(params.TextDocument.URI)
|
|
text, err := s.changedText(ctx, uri, params.ContentChanges)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c := source.FileModification{
|
|
URI: uri,
|
|
Action: source.Change,
|
|
Version: params.TextDocument.Version,
|
|
Text: text,
|
|
}
|
|
snapshot, err := s.didModifyFile(ctx, c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// 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, snapshot, uri) {
|
|
if err := s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
|
|
Message: fmt.Sprintf("Do not edit this file! %s is a generated file.", uri.Filename()),
|
|
Type: protocol.Warning,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) didChangeWatchedFiles(ctx context.Context, params *protocol.DidChangeWatchedFilesParams) error {
|
|
// Keep track of each change's view and final snapshot.
|
|
views := make(map[source.View]source.Snapshot)
|
|
for _, change := range params.Changes {
|
|
uri := span.NewURI(change.URI)
|
|
ctx := telemetry.File.With(ctx, uri)
|
|
|
|
// Do nothing if the file is open in the editor.
|
|
// The editor is the source of truth.
|
|
if s.session.IsOpen(uri) {
|
|
continue
|
|
}
|
|
snapshots, err := s.session.DidModifyFile(ctx, source.FileModification{
|
|
URI: uri,
|
|
Action: changeTypeToFileAction(change.Type),
|
|
OnDisk: true,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
snapshot, _, err := snapshotOf(s.session, uri, snapshots)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
views[snapshot.View()] = snapshot
|
|
}
|
|
// Diagnose all resulting snapshots.
|
|
for _, snapshot := range views {
|
|
go s.diagnoseSnapshot(snapshot)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) didSave(ctx context.Context, params *protocol.DidSaveTextDocumentParams) error {
|
|
c := source.FileModification{
|
|
URI: span.NewURI(params.TextDocument.URI),
|
|
Action: source.Save,
|
|
Version: params.TextDocument.Version,
|
|
}
|
|
if params.Text != nil {
|
|
c.Text = []byte(*params.Text)
|
|
}
|
|
_, err := s.didModifyFile(ctx, c)
|
|
return err
|
|
}
|
|
|
|
func (s *Server) didClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
|
|
_, err := s.didModifyFile(ctx, source.FileModification{
|
|
URI: span.NewURI(params.TextDocument.URI),
|
|
Action: source.Close,
|
|
Version: -1,
|
|
Text: nil,
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (s *Server) didModifyFile(ctx context.Context, c source.FileModification) (source.Snapshot, error) {
|
|
snapshots, err := s.session.DidModifyFile(ctx, c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
snapshot, _, err := snapshotOf(s.session, c.URI, snapshots)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch c.Action {
|
|
case source.Save:
|
|
// If we're saving a go.mod file, all of the metadata has been invalidated,
|
|
// so we need to run diagnostics and make sure that they cannot be canceled.
|
|
fh, err := snapshot.GetFile(c.URI)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if fh.Identity().Kind == source.Mod {
|
|
go s.diagnoseDetached(snapshot)
|
|
}
|
|
default:
|
|
go s.diagnoseSnapshot(snapshot)
|
|
}
|
|
|
|
return snapshot, nil
|
|
}
|
|
|
|
// snapshotOf returns the snapshot corresponding to the view for the given file URI.
|
|
func snapshotOf(session source.Session, uri span.URI, snapshots []source.Snapshot) (source.Snapshot, source.View, error) {
|
|
view, err := session.ViewOf(uri)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
for _, s := range snapshots {
|
|
if s.View() == view {
|
|
return s, view, nil
|
|
}
|
|
}
|
|
return nil, nil, errors.Errorf("bestSnapshot: no snapshot for %s", uri)
|
|
}
|
|
|
|
func (s *Server) wasFirstChange(uri span.URI) bool {
|
|
if s.changedFiles == nil {
|
|
s.changedFiles = make(map[span.URI]struct{})
|
|
}
|
|
_, ok := s.changedFiles[uri]
|
|
return ok
|
|
}
|
|
|
|
func (s *Server) changedText(ctx context.Context, uri span.URI, changes []protocol.TextDocumentContentChangeEvent) ([]byte, error) {
|
|
if len(changes) == 0 {
|
|
return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "no content changes provided")
|
|
}
|
|
|
|
// Check if the client sent the full content of the file.
|
|
// We accept a full content change even if the server expected incremental changes.
|
|
if len(changes) == 1 && changes[0].Range == nil && changes[0].RangeLength == 0 {
|
|
return []byte(changes[0].Text), nil
|
|
}
|
|
return s.applyIncrementalChanges(ctx, uri, changes)
|
|
}
|
|
|
|
func (s *Server) applyIncrementalChanges(ctx context.Context, uri span.URI, changes []protocol.TextDocumentContentChangeEvent) ([]byte, error) {
|
|
content, _, err := s.session.GetFile(uri).Read(ctx)
|
|
if err != nil {
|
|
return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found (%v)", err)
|
|
}
|
|
for _, change := range changes {
|
|
// Make sure to update column mapper along with the content.
|
|
converter := span.NewContentConverter(uri.Filename(), content)
|
|
m := &protocol.ColumnMapper{
|
|
URI: uri,
|
|
Converter: converter,
|
|
Content: content,
|
|
}
|
|
if change.Range == nil {
|
|
return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "unexpected nil range for change")
|
|
}
|
|
spn, err := m.RangeSpan(*change.Range)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !spn.HasOffset() {
|
|
return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "invalid range for content change")
|
|
}
|
|
start, end := spn.Start().Offset(), spn.End().Offset()
|
|
if end < start {
|
|
return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "invalid range for content change")
|
|
}
|
|
var buf bytes.Buffer
|
|
buf.Write(content[:start])
|
|
buf.WriteString(change.Text)
|
|
buf.Write(content[end:])
|
|
content = buf.Bytes()
|
|
}
|
|
return content, nil
|
|
}
|
|
|
|
func changeTypeToFileAction(ct protocol.FileChangeType) source.FileAction {
|
|
switch ct {
|
|
case protocol.Changed:
|
|
return source.Change
|
|
case protocol.Created:
|
|
return source.Create
|
|
case protocol.Deleted:
|
|
return source.Delete
|
|
}
|
|
return source.UnknownFileAction
|
|
}
|