golang.org/x/tools/gopls@v0.15.3/internal/server/text_synchronization.go (about)

     1  // Copyright 2019 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package server
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"errors"
    11  	"fmt"
    12  	"path/filepath"
    13  	"sync"
    14  
    15  	"golang.org/x/tools/gopls/internal/cache"
    16  	"golang.org/x/tools/gopls/internal/file"
    17  	"golang.org/x/tools/gopls/internal/golang"
    18  	"golang.org/x/tools/gopls/internal/protocol"
    19  	"golang.org/x/tools/internal/event"
    20  	"golang.org/x/tools/internal/event/tag"
    21  	"golang.org/x/tools/internal/jsonrpc2"
    22  	"golang.org/x/tools/internal/xcontext"
    23  )
    24  
    25  // ModificationSource identifies the origin of a change.
    26  type ModificationSource int
    27  
    28  const (
    29  	// FromDidOpen is from a didOpen notification.
    30  	FromDidOpen = ModificationSource(iota)
    31  
    32  	// FromDidChange is from a didChange notification.
    33  	FromDidChange
    34  
    35  	// FromDidChangeWatchedFiles is from didChangeWatchedFiles notification.
    36  	FromDidChangeWatchedFiles
    37  
    38  	// FromDidSave is from a didSave notification.
    39  	FromDidSave
    40  
    41  	// FromDidClose is from a didClose notification.
    42  	FromDidClose
    43  
    44  	// FromDidChangeConfiguration is from a didChangeConfiguration notification.
    45  	FromDidChangeConfiguration
    46  
    47  	// FromRegenerateCgo refers to file modifications caused by regenerating
    48  	// the cgo sources for the workspace.
    49  	FromRegenerateCgo
    50  
    51  	// FromInitialWorkspaceLoad refers to the loading of all packages in the
    52  	// workspace when the view is first created.
    53  	FromInitialWorkspaceLoad
    54  
    55  	// FromCheckUpgrades refers to state changes resulting from the CheckUpgrades
    56  	// command, which queries module upgrades.
    57  	FromCheckUpgrades
    58  
    59  	// FromResetGoModDiagnostics refers to state changes resulting from the
    60  	// ResetGoModDiagnostics command.
    61  	FromResetGoModDiagnostics
    62  
    63  	// FromToggleGCDetails refers to state changes resulting from toggling
    64  	// gc_details on or off for a package.
    65  	FromToggleGCDetails
    66  )
    67  
    68  func (m ModificationSource) String() string {
    69  	switch m {
    70  	case FromDidOpen:
    71  		return "opened files"
    72  	case FromDidChange:
    73  		return "changed files"
    74  	case FromDidChangeWatchedFiles:
    75  		return "files changed on disk"
    76  	case FromDidSave:
    77  		return "saved files"
    78  	case FromDidClose:
    79  		return "close files"
    80  	case FromRegenerateCgo:
    81  		return "regenerate cgo"
    82  	case FromInitialWorkspaceLoad:
    83  		return "initial workspace load"
    84  	case FromCheckUpgrades:
    85  		return "from check upgrades"
    86  	case FromResetGoModDiagnostics:
    87  		return "from resetting go.mod diagnostics"
    88  	default:
    89  		return "unknown file modification"
    90  	}
    91  }
    92  
    93  func (s *server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error {
    94  	ctx, done := event.Start(ctx, "lsp.Server.didOpen", tag.URI.Of(params.TextDocument.URI))
    95  	defer done()
    96  
    97  	uri := params.TextDocument.URI
    98  	// There may not be any matching view in the current session. If that's
    99  	// the case, try creating a new view based on the opened file path.
   100  	//
   101  	// TODO(golang/go#57979): revisit creating a folder here. We should separate
   102  	// the logic for managing folders from the logic for managing views. But it
   103  	// does make sense to ensure at least one workspace folder the first time a
   104  	// file is opened, and we can't do that inside didModifyFiles because we
   105  	// don't want to request configuration while holding a lock.
   106  	if len(s.session.Views()) == 0 {
   107  		dir := filepath.Dir(uri.Path())
   108  		s.addFolders(ctx, []protocol.WorkspaceFolder{{
   109  			URI:  string(protocol.URIFromPath(dir)),
   110  			Name: filepath.Base(dir),
   111  		}})
   112  	}
   113  	return s.didModifyFiles(ctx, []file.Modification{{
   114  		URI:        uri,
   115  		Action:     file.Open,
   116  		Version:    params.TextDocument.Version,
   117  		Text:       []byte(params.TextDocument.Text),
   118  		LanguageID: params.TextDocument.LanguageID,
   119  	}}, FromDidOpen)
   120  }
   121  
   122  func (s *server) DidChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error {
   123  	ctx, done := event.Start(ctx, "lsp.Server.didChange", tag.URI.Of(params.TextDocument.URI))
   124  	defer done()
   125  
   126  	uri := params.TextDocument.URI
   127  	text, err := s.changedText(ctx, uri, params.ContentChanges)
   128  	if err != nil {
   129  		return err
   130  	}
   131  	c := file.Modification{
   132  		URI:     uri,
   133  		Action:  file.Change,
   134  		Version: params.TextDocument.Version,
   135  		Text:    text,
   136  	}
   137  	if err := s.didModifyFiles(ctx, []file.Modification{c}, FromDidChange); err != nil {
   138  		return err
   139  	}
   140  	return s.warnAboutModifyingGeneratedFiles(ctx, uri)
   141  }
   142  
   143  // warnAboutModifyingGeneratedFiles shows a warning if a user tries to edit a
   144  // generated file for the first time.
   145  func (s *server) warnAboutModifyingGeneratedFiles(ctx context.Context, uri protocol.DocumentURI) error {
   146  	s.changedFilesMu.Lock()
   147  	_, ok := s.changedFiles[uri]
   148  	if !ok {
   149  		s.changedFiles[uri] = struct{}{}
   150  	}
   151  	s.changedFilesMu.Unlock()
   152  
   153  	// This file has already been edited before.
   154  	if ok {
   155  		return nil
   156  	}
   157  
   158  	// Ideally, we should be able to specify that a generated file should
   159  	// be opened as read-only. Tell the user that they should not be
   160  	// editing a generated file.
   161  	snapshot, release, err := s.session.SnapshotOf(ctx, uri)
   162  	if err != nil {
   163  		return err
   164  	}
   165  	isGenerated := golang.IsGenerated(ctx, snapshot, uri)
   166  	release()
   167  
   168  	if isGenerated {
   169  		msg := fmt.Sprintf("Do not edit this file! %s is a generated file.", uri.Path())
   170  		showMessage(ctx, s.client, protocol.Warning, msg)
   171  	}
   172  	return nil
   173  }
   174  
   175  func (s *server) DidChangeWatchedFiles(ctx context.Context, params *protocol.DidChangeWatchedFilesParams) error {
   176  	ctx, done := event.Start(ctx, "lsp.Server.didChangeWatchedFiles")
   177  	defer done()
   178  
   179  	var modifications []file.Modification
   180  	for _, change := range params.Changes {
   181  		action := changeTypeToFileAction(change.Type)
   182  		modifications = append(modifications, file.Modification{
   183  			URI:    change.URI,
   184  			Action: action,
   185  			OnDisk: true,
   186  		})
   187  	}
   188  	return s.didModifyFiles(ctx, modifications, FromDidChangeWatchedFiles)
   189  }
   190  
   191  func (s *server) DidSave(ctx context.Context, params *protocol.DidSaveTextDocumentParams) error {
   192  	ctx, done := event.Start(ctx, "lsp.Server.didSave", tag.URI.Of(params.TextDocument.URI))
   193  	defer done()
   194  
   195  	c := file.Modification{
   196  		URI:    params.TextDocument.URI,
   197  		Action: file.Save,
   198  	}
   199  	if params.Text != nil {
   200  		c.Text = []byte(*params.Text)
   201  	}
   202  	return s.didModifyFiles(ctx, []file.Modification{c}, FromDidSave)
   203  }
   204  
   205  func (s *server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
   206  	ctx, done := event.Start(ctx, "lsp.Server.didClose", tag.URI.Of(params.TextDocument.URI))
   207  	defer done()
   208  
   209  	return s.didModifyFiles(ctx, []file.Modification{
   210  		{
   211  			URI:     params.TextDocument.URI,
   212  			Action:  file.Close,
   213  			Version: -1,
   214  			Text:    nil,
   215  		},
   216  	}, FromDidClose)
   217  }
   218  
   219  func (s *server) didModifyFiles(ctx context.Context, modifications []file.Modification, cause ModificationSource) error {
   220  	// wg guards two conditions:
   221  	//  1. didModifyFiles is complete
   222  	//  2. the goroutine diagnosing changes on behalf of didModifyFiles is
   223  	//     complete, if it was started
   224  	//
   225  	// Both conditions must be satisfied for the purpose of testing: we don't
   226  	// want to observe the completion of change processing until we have received
   227  	// all diagnostics as well as all server->client notifications done on behalf
   228  	// of this function.
   229  	var wg sync.WaitGroup
   230  	wg.Add(1)
   231  	defer wg.Done()
   232  
   233  	if s.Options().VerboseWorkDoneProgress {
   234  		work := s.progress.Start(ctx, DiagnosticWorkTitle(cause), "Calculating file diagnostics...", nil, nil)
   235  		go func() {
   236  			wg.Wait()
   237  			work.End(ctx, "Done.")
   238  		}()
   239  	}
   240  
   241  	s.stateMu.Lock()
   242  	if s.state >= serverShutDown {
   243  		// This state check does not prevent races below, and exists only to
   244  		// produce a better error message. The actual race to the cache should be
   245  		// guarded by Session.viewMu.
   246  		s.stateMu.Unlock()
   247  		return errors.New("server is shut down")
   248  	}
   249  	s.stateMu.Unlock()
   250  
   251  	// If the set of changes included directories, expand those directories
   252  	// to their files.
   253  	modifications = s.session.ExpandModificationsToDirectories(ctx, modifications)
   254  
   255  	viewsToDiagnose, err := s.session.DidModifyFiles(ctx, modifications)
   256  	if err != nil {
   257  		return err
   258  	}
   259  
   260  	// golang/go#50267: diagnostics should be re-sent after each change.
   261  	for _, mod := range modifications {
   262  		s.mustPublishDiagnostics(mod.URI)
   263  	}
   264  
   265  	modCtx, modID := s.needsDiagnosis(ctx, viewsToDiagnose)
   266  
   267  	wg.Add(1)
   268  	go func() {
   269  		s.diagnoseChangedViews(modCtx, modID, viewsToDiagnose, cause)
   270  		wg.Done()
   271  	}()
   272  
   273  	// After any file modifications, we need to update our watched files,
   274  	// in case something changed. Compute the new set of directories to watch,
   275  	// and if it differs from the current set, send updated registrations.
   276  	return s.updateWatchedDirectories(ctx)
   277  }
   278  
   279  // needsDiagnosis records the given views as needing diagnosis, returning the
   280  // context and modification id to use for said diagnosis.
   281  //
   282  // Only the keys of viewsToDiagnose are used; the changed files are irrelevant.
   283  func (s *server) needsDiagnosis(ctx context.Context, viewsToDiagnose map[*cache.View][]protocol.DocumentURI) (context.Context, uint64) {
   284  	s.modificationMu.Lock()
   285  	defer s.modificationMu.Unlock()
   286  	if s.cancelPrevDiagnostics != nil {
   287  		s.cancelPrevDiagnostics()
   288  	}
   289  	modCtx := xcontext.Detach(ctx)
   290  	modCtx, s.cancelPrevDiagnostics = context.WithCancel(modCtx)
   291  	s.lastModificationID++
   292  	modID := s.lastModificationID
   293  
   294  	for v := range viewsToDiagnose {
   295  		if needs, ok := s.viewsToDiagnose[v]; !ok || needs < modID {
   296  			s.viewsToDiagnose[v] = modID
   297  		}
   298  	}
   299  	return modCtx, modID
   300  }
   301  
   302  // DiagnosticWorkTitle returns the title of the diagnostic work resulting from a
   303  // file change originating from the given cause.
   304  func DiagnosticWorkTitle(cause ModificationSource) string {
   305  	return fmt.Sprintf("diagnosing %v", cause)
   306  }
   307  
   308  func (s *server) changedText(ctx context.Context, uri protocol.DocumentURI, changes []protocol.TextDocumentContentChangeEvent) ([]byte, error) {
   309  	if len(changes) == 0 {
   310  		return nil, fmt.Errorf("%w: no content changes provided", jsonrpc2.ErrInternal)
   311  	}
   312  
   313  	// Check if the client sent the full content of the file.
   314  	// We accept a full content change even if the server expected incremental changes.
   315  	if len(changes) == 1 && changes[0].Range == nil && changes[0].RangeLength == 0 {
   316  		return []byte(changes[0].Text), nil
   317  	}
   318  	return s.applyIncrementalChanges(ctx, uri, changes)
   319  }
   320  
   321  func (s *server) applyIncrementalChanges(ctx context.Context, uri protocol.DocumentURI, changes []protocol.TextDocumentContentChangeEvent) ([]byte, error) {
   322  	fh, err := s.session.ReadFile(ctx, uri)
   323  	if err != nil {
   324  		return nil, err
   325  	}
   326  	content, err := fh.Content()
   327  	if err != nil {
   328  		return nil, fmt.Errorf("%w: file not found (%v)", jsonrpc2.ErrInternal, err)
   329  	}
   330  	for _, change := range changes {
   331  		// TODO(adonovan): refactor to use diff.Apply, which is robust w.r.t.
   332  		// out-of-order or overlapping changes---and much more efficient.
   333  
   334  		// Make sure to update mapper along with the content.
   335  		m := protocol.NewMapper(uri, content)
   336  		if change.Range == nil {
   337  			return nil, fmt.Errorf("%w: unexpected nil range for change", jsonrpc2.ErrInternal)
   338  		}
   339  		start, end, err := m.RangeOffsets(*change.Range)
   340  		if err != nil {
   341  			return nil, err
   342  		}
   343  		if end < start {
   344  			return nil, fmt.Errorf("%w: invalid range for content change", jsonrpc2.ErrInternal)
   345  		}
   346  		var buf bytes.Buffer
   347  		buf.Write(content[:start])
   348  		buf.WriteString(change.Text)
   349  		buf.Write(content[end:])
   350  		content = buf.Bytes()
   351  	}
   352  	return content, nil
   353  }
   354  
   355  func changeTypeToFileAction(ct protocol.FileChangeType) file.Action {
   356  	switch ct {
   357  	case protocol.Changed:
   358  		return file.Change
   359  	case protocol.Created:
   360  		return file.Create
   361  	case protocol.Deleted:
   362  		return file.Delete
   363  	}
   364  	return file.UnknownAction
   365  }