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