aboutsummaryrefslogtreecommitdiff
path: root/internal/lsp/text_synchronization.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/lsp/text_synchronization.go')
-rw-r--r--internal/lsp/text_synchronization.go382
1 files changed, 0 insertions, 382 deletions
diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go
deleted file mode 100644
index d9a696140..000000000
--- a/internal/lsp/text_synchronization.go
+++ /dev/null
@@ -1,382 +0,0 @@
-// 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"
- "path/filepath"
- "time"
-
- "golang.org/x/tools/internal/event"
- "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"
- "golang.org/x/tools/internal/xcontext"
- errors "golang.org/x/xerrors"
-)
-
-// ModificationSource identifies the originating cause of a file modification.
-type ModificationSource int
-
-const (
- // FromDidOpen is a file modification caused by opening a file.
- FromDidOpen = ModificationSource(iota)
-
- // FromDidChange is a file modification caused by changing a file.
- FromDidChange
-
- // FromDidChangeWatchedFiles is a file modification caused by a change to a
- // watched file.
- FromDidChangeWatchedFiles
-
- // FromDidSave is a file modification caused by a file save.
- FromDidSave
-
- // FromDidClose is a file modification caused by closing a file.
- FromDidClose
-
- // FromRegenerateCgo refers to file modifications caused by regenerating
- // the cgo sources for the workspace.
- FromRegenerateCgo
-
- // FromInitialWorkspaceLoad refers to the loading of all packages in the
- // workspace when the view is first created.
- FromInitialWorkspaceLoad
-)
-
-func (m ModificationSource) String() string {
- switch m {
- case FromDidOpen:
- return "opened files"
- case FromDidChange:
- return "changed files"
- case FromDidChangeWatchedFiles:
- return "files changed on disk"
- case FromDidSave:
- return "saved files"
- case FromDidClose:
- return "close files"
- case FromRegenerateCgo:
- return "regenerate cgo"
- case FromInitialWorkspaceLoad:
- return "initial workspace load"
- default:
- return "unknown file modification"
- }
-}
-
-func (s *Server) didOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error {
- uri := params.TextDocument.URI.SpanURI()
- if !uri.IsFile() {
- return nil
- }
- // There may not be any matching view in the current session. If that's
- // the case, try creating a new view based on the opened file path.
- //
- // TODO(rstambler): This seems like it would continuously add new
- // views, but it won't because ViewOf only returns an error when there
- // are no views in the session. I don't know if that logic should go
- // here, or if we can continue to rely on that implementation detail.
- if _, err := s.session.ViewOf(uri); err != nil {
- dir := filepath.Dir(uri.Filename())
- if err := s.addFolders(ctx, []protocol.WorkspaceFolder{{
- URI: string(protocol.URIFromPath(dir)),
- Name: filepath.Base(dir),
- }}); err != nil {
- return err
- }
- }
- return s.didModifyFiles(ctx, []source.FileModification{{
- URI: uri,
- Action: source.Open,
- Version: params.TextDocument.Version,
- Text: []byte(params.TextDocument.Text),
- LanguageID: params.TextDocument.LanguageID,
- }}, FromDidOpen)
-}
-
-func (s *Server) didChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error {
- uri := params.TextDocument.URI.SpanURI()
- if !uri.IsFile() {
- return nil
- }
-
- 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,
- }
- if err := s.didModifyFiles(ctx, []source.FileModification{c}, FromDidChange); err != nil {
- return err
- }
- return s.warnAboutModifyingGeneratedFiles(ctx, uri)
-}
-
-// warnAboutModifyingGeneratedFiles shows a warning if a user tries to edit a
-// generated file for the first time.
-func (s *Server) warnAboutModifyingGeneratedFiles(ctx context.Context, uri span.URI) error {
- s.changedFilesMu.Lock()
- _, ok := s.changedFiles[uri]
- if !ok {
- s.changedFiles[uri] = struct{}{}
- }
- s.changedFilesMu.Unlock()
-
- // This file has already been edited before.
- if ok {
- return nil
- }
-
- // 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.
- view, err := s.session.ViewOf(uri)
- if err != nil {
- return err
- }
- snapshot, release := view.Snapshot(ctx)
- isGenerated := source.IsGenerated(ctx, snapshot, uri)
- release()
-
- if !isGenerated {
- return nil
- }
- return s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
- Message: fmt.Sprintf("Do not edit this file! %s is a generated file.", uri.Filename()),
- Type: protocol.Warning,
- })
-}
-
-func (s *Server) didChangeWatchedFiles(ctx context.Context, params *protocol.DidChangeWatchedFilesParams) error {
- var modifications []source.FileModification
- for _, change := range params.Changes {
- uri := change.URI.SpanURI()
- if !uri.IsFile() {
- continue
- }
- action := changeTypeToFileAction(change.Type)
- modifications = append(modifications, source.FileModification{
- URI: uri,
- Action: action,
- OnDisk: true,
- })
- }
- return s.didModifyFiles(ctx, modifications, FromDidChangeWatchedFiles)
-}
-
-func (s *Server) didSave(ctx context.Context, params *protocol.DidSaveTextDocumentParams) error {
- uri := params.TextDocument.URI.SpanURI()
- if !uri.IsFile() {
- return nil
- }
- c := source.FileModification{
- URI: uri,
- Action: source.Save,
- }
- if params.Text != nil {
- c.Text = []byte(*params.Text)
- }
- return s.didModifyFiles(ctx, []source.FileModification{c}, FromDidSave)
-}
-
-func (s *Server) didClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
- uri := params.TextDocument.URI.SpanURI()
- if !uri.IsFile() {
- return nil
- }
- return s.didModifyFiles(ctx, []source.FileModification{
- {
- URI: uri,
- Action: source.Close,
- Version: -1,
- Text: nil,
- },
- }, FromDidClose)
-}
-
-func (s *Server) didModifyFiles(ctx context.Context, modifications []source.FileModification, cause ModificationSource) error {
- diagnoseDone := make(chan struct{})
- if s.session.Options().VerboseWorkDoneProgress {
- work := s.progress.Start(ctx, DiagnosticWorkTitle(cause), "Calculating file diagnostics...", nil, nil)
- defer func() {
- go func() {
- <-diagnoseDone
- work.End("Done.")
- }()
- }()
- }
-
- onDisk := cause == FromDidChangeWatchedFiles
- delay := s.session.Options().ExperimentalWatchedFileDelay
- s.fileChangeMu.Lock()
- defer s.fileChangeMu.Unlock()
- if !onDisk || delay == 0 {
- // No delay: process the modifications immediately.
- return s.processModifications(ctx, modifications, onDisk, diagnoseDone)
- }
- // Debounce and batch up pending modifications from watched files.
- pending := &pendingModificationSet{
- diagnoseDone: diagnoseDone,
- changes: modifications,
- }
- // Invariant: changes appended to s.pendingOnDiskChanges are eventually
- // handled in the order they arrive. This guarantee is only partially
- // enforced here. Specifically:
- // 1. s.fileChangesMu ensures that the append below happens in the order
- // notifications were received, so that the changes within each batch are
- // ordered properly.
- // 2. The debounced func below holds s.fileChangesMu while processing all
- // changes in s.pendingOnDiskChanges, ensuring that no batches are
- // processed out of order.
- // 3. Session.ExpandModificationsToDirectories and Session.DidModifyFiles
- // process changes in order.
- s.pendingOnDiskChanges = append(s.pendingOnDiskChanges, pending)
- ctx = xcontext.Detach(ctx)
- okc := s.watchedFileDebouncer.debounce("", 0, time.After(delay))
- go func() {
- if ok := <-okc; !ok {
- return
- }
- s.fileChangeMu.Lock()
- var allChanges []source.FileModification
- // For accurate progress notifications, we must notify all goroutines
- // waiting for the diagnose pass following a didChangeWatchedFiles
- // notification. This is necessary for regtest assertions.
- var dones []chan struct{}
- for _, pending := range s.pendingOnDiskChanges {
- allChanges = append(allChanges, pending.changes...)
- dones = append(dones, pending.diagnoseDone)
- }
-
- allDone := make(chan struct{})
- if err := s.processModifications(ctx, allChanges, onDisk, allDone); err != nil {
- event.Error(ctx, "processing delayed file changes", err)
- }
- s.pendingOnDiskChanges = nil
- s.fileChangeMu.Unlock()
- <-allDone
- for _, done := range dones {
- close(done)
- }
- }()
- return nil
-}
-
-// processModifications update server state to reflect file changes, and
-// triggers diagnostics to run asynchronously. The diagnoseDone channel will be
-// closed once diagnostics complete.
-func (s *Server) processModifications(ctx context.Context, modifications []source.FileModification, onDisk bool, diagnoseDone chan struct{}) error {
- s.stateMu.Lock()
- if s.state >= serverShutDown {
- // This state check does not prevent races below, and exists only to
- // produce a better error message. The actual race to the cache should be
- // guarded by Session.viewMu.
- s.stateMu.Unlock()
- close(diagnoseDone)
- return errors.New("server is shut down")
- }
- s.stateMu.Unlock()
- // If the set of changes included directories, expand those directories
- // to their files.
- modifications = s.session.ExpandModificationsToDirectories(ctx, modifications)
-
- snapshots, releases, err := s.session.DidModifyFiles(ctx, modifications)
- if err != nil {
- close(diagnoseDone)
- return err
- }
-
- go func() {
- s.diagnoseSnapshots(snapshots, onDisk)
- for _, release := range releases {
- release()
- }
- close(diagnoseDone)
- }()
-
- // After any file modifications, we need to update our watched files,
- // in case something changed. Compute the new set of directories to watch,
- // and if it differs from the current set, send updated registrations.
- return s.updateWatchedDirectories(ctx)
-}
-
-// DiagnosticWorkTitle returns the title of the diagnostic work resulting from a
-// file change originating from the given cause.
-func DiagnosticWorkTitle(cause ModificationSource) string {
- return fmt.Sprintf("diagnosing %v", cause)
-}
-
-func (s *Server) changedText(ctx context.Context, uri span.URI, changes []protocol.TextDocumentContentChangeEvent) ([]byte, error) {
- if len(changes) == 0 {
- return nil, errors.Errorf("%w: no content changes provided", jsonrpc2.ErrInternal)
- }
-
- // 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) {
- fh, err := s.session.GetFile(ctx, uri)
- if err != nil {
- return nil, err
- }
- content, err := fh.Read()
- if err != nil {
- return nil, errors.Errorf("%w: file not found (%v)", jsonrpc2.ErrInternal, 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, errors.Errorf("%w: unexpected nil range for change", jsonrpc2.ErrInternal)
- }
- spn, err := m.RangeSpan(*change.Range)
- if err != nil {
- return nil, err
- }
- if !spn.HasOffset() {
- return nil, errors.Errorf("%w: invalid range for content change", jsonrpc2.ErrInternal)
- }
- start, end := spn.Start().Offset(), spn.End().Offset()
- if end < start {
- return nil, errors.Errorf("%w: invalid range for content change", jsonrpc2.ErrInternal)
- }
- 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
-}