1
0
mirror of https://github.com/golang/go synced 2024-11-19 02:24:41 -07:00
go/internal/lsp/text_synchronization.go
Rebecca Stambler ca0407e66b internal/lsp: return snapshots from text modifications
Eliminate the file watcher, since it led to a lot of confusion and
difficulty reasoning about the flow of a file action. This change splits
a file invalidation into the two logical steps - 1) things that affect
the overlay, and 2) things that affect the view. It is based on top of
CL 211757, so the diffs will look better once that CL is merged.

Change-Id: I277475569b61f3c80feaa6b6fe457b4bace82e35
Reviewed-on: https://go-review.googlesource.com/c/tools/+/211777
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
2019-12-18 22:53:40 +00:00

178 lines
5.5 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/span"
errors "golang.org/x/xerrors"
)
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)
snapshots, 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,
})
if err != nil {
return err
}
snapshot, view, err := snapshotOf(s.session, uri, snapshots)
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(snapshot, f)
}
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
}
snapshots, err := s.session.DidModifyFile(ctx, source.FileModification{
URI: uri,
Action: source.Change,
Version: params.TextDocument.Version,
Text: text,
})
if err != nil {
return err
}
snapshot, view, err := snapshotOf(s.session, uri, snapshots)
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, 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,
})
}
f, err := view.GetFile(ctx, uri)
if err != nil {
return err
}
// Always update diagnostics after a file change.
return s.diagnose(snapshot, f)
}
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.session.DidModifyFile(ctx, c)
return err
}
func (s *Server) didClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
_, err := s.session.DidModifyFile(ctx, source.FileModification{
URI: span.NewURI(params.TextDocument.URI),
Action: source.Close,
Version: -1,
Text: nil,
})
return err
}
// 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
}
// We only accept an incremental change if that's what the server expects.
if s.session.Options().TextDocumentSyncKind == protocol.Full {
return nil, errors.Errorf("expected a full content change, received incremental changes for %s", uri)
}
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, source.UnknownKind).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
}