github.com/april1989/origin-go-tools@v0.0.32/internal/lsp/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 lsp
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"fmt"
    11  	"path/filepath"
    12  	"sync"
    13  
    14  	"github.com/april1989/origin-go-tools/internal/jsonrpc2"
    15  	"github.com/april1989/origin-go-tools/internal/lsp/protocol"
    16  	"github.com/april1989/origin-go-tools/internal/lsp/source"
    17  	"github.com/april1989/origin-go-tools/internal/span"
    18  	errors "golang.org/x/xerrors"
    19  )
    20  
    21  // ModificationSource identifies the originating cause of a file modification.
    22  type ModificationSource int
    23  
    24  const (
    25  	// FromDidOpen is a file modification caused by opening a file.
    26  	FromDidOpen = ModificationSource(iota)
    27  
    28  	// FromDidChange is a file modification caused by changing a file.
    29  	FromDidChange
    30  
    31  	// FromDidChangeWatchedFiles is a file modification caused by a change to a
    32  	// watched file.
    33  	FromDidChangeWatchedFiles
    34  
    35  	// FromDidSave is a file modification caused by a file save.
    36  	FromDidSave
    37  
    38  	// FromDidClose is a file modification caused by closing a file.
    39  	FromDidClose
    40  
    41  	// FromRegenerateCgo refers to file modifications caused by regenerating
    42  	// the cgo sources for the workspace.
    43  	FromRegenerateCgo
    44  
    45  	// FromInitialWorkspaceLoad refers to the loading of all packages in the
    46  	// workspace when the view is first created.
    47  	FromInitialWorkspaceLoad
    48  )
    49  
    50  func (m ModificationSource) String() string {
    51  	switch m {
    52  	case FromDidOpen:
    53  		return "opened files"
    54  	case FromDidChange:
    55  		return "changed files"
    56  	case FromDidChangeWatchedFiles:
    57  		return "files changed on disk"
    58  	case FromDidSave:
    59  		return "saved files"
    60  	case FromDidClose:
    61  		return "close files"
    62  	case FromRegenerateCgo:
    63  		return "regenerate cgo"
    64  	case FromInitialWorkspaceLoad:
    65  		return "initial workspace load"
    66  	default:
    67  		return "unknown file modification"
    68  	}
    69  }
    70  
    71  func (s *Server) didOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error {
    72  	uri := params.TextDocument.URI.SpanURI()
    73  	if !uri.IsFile() {
    74  		return nil
    75  	}
    76  	// There may not be any matching view in the current session. If that's
    77  	// the case, try creating a new view based on the opened file path.
    78  	//
    79  	// TODO(rstambler): This seems like it would continuously add new
    80  	// views, but it won't because ViewOf only returns an error when there
    81  	// are no views in the session. I don't know if that logic should go
    82  	// here, or if we can continue to rely on that implementation detail.
    83  	if _, err := s.session.ViewOf(uri); err != nil {
    84  		dir := filepath.Dir(uri.Filename())
    85  		if err := s.addFolders(ctx, []protocol.WorkspaceFolder{{
    86  			URI:  string(protocol.URIFromPath(dir)),
    87  			Name: filepath.Base(dir),
    88  		}}); err != nil {
    89  			return err
    90  		}
    91  	}
    92  
    93  	return s.didModifyFiles(ctx, []source.FileModification{
    94  		{
    95  			URI:        uri,
    96  			Action:     source.Open,
    97  			Version:    params.TextDocument.Version,
    98  			Text:       []byte(params.TextDocument.Text),
    99  			LanguageID: params.TextDocument.LanguageID,
   100  		},
   101  	}, FromDidOpen)
   102  }
   103  
   104  func (s *Server) didChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error {
   105  	uri := params.TextDocument.URI.SpanURI()
   106  	if !uri.IsFile() {
   107  		return nil
   108  	}
   109  
   110  	text, err := s.changedText(ctx, uri, params.ContentChanges)
   111  	if err != nil {
   112  		return err
   113  	}
   114  	c := source.FileModification{
   115  		URI:     uri,
   116  		Action:  source.Change,
   117  		Version: params.TextDocument.Version,
   118  		Text:    text,
   119  	}
   120  	if err := s.didModifyFiles(ctx, []source.FileModification{c}, FromDidChange); err != nil {
   121  		return err
   122  	}
   123  
   124  	s.changedFilesMu.Lock()
   125  	defer s.changedFilesMu.Unlock()
   126  
   127  	s.changedFiles[uri] = struct{}{}
   128  	return nil
   129  }
   130  
   131  func (s *Server) didChangeWatchedFiles(ctx context.Context, params *protocol.DidChangeWatchedFilesParams) error {
   132  	var modifications []source.FileModification
   133  	for _, change := range params.Changes {
   134  		uri := change.URI.SpanURI()
   135  		if !uri.IsFile() {
   136  			continue
   137  		}
   138  		action := changeTypeToFileAction(change.Type)
   139  		modifications = append(modifications, source.FileModification{
   140  			URI:    uri,
   141  			Action: action,
   142  			OnDisk: true,
   143  		})
   144  	}
   145  	return s.didModifyFiles(ctx, modifications, FromDidChangeWatchedFiles)
   146  }
   147  
   148  func (s *Server) didSave(ctx context.Context, params *protocol.DidSaveTextDocumentParams) error {
   149  	uri := params.TextDocument.URI.SpanURI()
   150  	if !uri.IsFile() {
   151  		return nil
   152  	}
   153  	c := source.FileModification{
   154  		URI:     uri,
   155  		Action:  source.Save,
   156  		Version: params.TextDocument.Version,
   157  	}
   158  	if params.Text != nil {
   159  		c.Text = []byte(*params.Text)
   160  	}
   161  	return s.didModifyFiles(ctx, []source.FileModification{c}, FromDidSave)
   162  }
   163  
   164  func (s *Server) didClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
   165  	uri := params.TextDocument.URI.SpanURI()
   166  	if !uri.IsFile() {
   167  		return nil
   168  	}
   169  	return s.didModifyFiles(ctx, []source.FileModification{
   170  		{
   171  			URI:     uri,
   172  			Action:  source.Close,
   173  			Version: -1,
   174  			Text:    nil,
   175  		},
   176  	}, FromDidClose)
   177  }
   178  
   179  func (s *Server) didModifyFiles(ctx context.Context, modifications []source.FileModification, cause ModificationSource) error {
   180  	// diagnosticWG tracks outstanding diagnostic work as a result of this file
   181  	// modification.
   182  	var diagnosticWG sync.WaitGroup
   183  	if s.session.Options().VerboseWorkDoneProgress {
   184  		work := s.progress.start(ctx, DiagnosticWorkTitle(cause), "Calculating file diagnostics...", nil, nil)
   185  		defer func() {
   186  			go func() {
   187  				diagnosticWG.Wait()
   188  				work.end("Done.")
   189  			}()
   190  		}()
   191  	}
   192  	snapshots, releases, deletions, err := s.session.DidModifyFiles(ctx, modifications)
   193  	if err != nil {
   194  		return err
   195  	}
   196  
   197  	for _, uri := range deletions {
   198  		if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{
   199  			URI:         protocol.URIFromSpanURI(uri),
   200  			Diagnostics: []protocol.Diagnostic{},
   201  			Version:     0,
   202  		}); err != nil {
   203  			return err
   204  		}
   205  	}
   206  	snapshotByURI := make(map[span.URI]source.Snapshot)
   207  	for _, c := range modifications {
   208  		snapshotByURI[c.URI] = nil
   209  	}
   210  	// Avoid diagnosing the same snapshot twice.
   211  	snapshotSet := make(map[source.Snapshot][]span.URI)
   212  	for uri := range snapshotByURI {
   213  		view, err := s.session.ViewOf(uri)
   214  		if err != nil {
   215  			return err
   216  		}
   217  		var snapshot source.Snapshot
   218  		for _, s := range snapshots {
   219  			if s.View() == view {
   220  				if snapshot != nil {
   221  					return errors.Errorf("duplicate snapshots for the same view")
   222  				}
   223  				snapshot = s
   224  			}
   225  		}
   226  		// If the file isn't in any known views (for example, if it's in a dependency),
   227  		// we may not have a snapshot to map it to. As a result, we won't try to
   228  		// diagnose it. TODO(rstambler): Figure out how to handle this better.
   229  		if snapshot == nil {
   230  			continue
   231  		}
   232  		snapshotSet[snapshot] = append(snapshotSet[snapshot], uri)
   233  		snapshotByURI[uri] = snapshot
   234  	}
   235  
   236  	for _, mod := range modifications {
   237  		if mod.OnDisk || mod.Action != source.Change {
   238  			continue
   239  		}
   240  		snapshot, ok := snapshotByURI[mod.URI]
   241  		if !ok {
   242  			continue
   243  		}
   244  		// Ideally, we should be able to specify that a generated file should be opened as read-only.
   245  		// Tell the user that they should not be editing a generated file.
   246  		if s.wasFirstChange(mod.URI) && source.IsGenerated(ctx, snapshot, mod.URI) {
   247  			if err := s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
   248  				Message: fmt.Sprintf("Do not edit this file! %s is a generated file.", mod.URI.Filename()),
   249  				Type:    protocol.Warning,
   250  			}); err != nil {
   251  				return err
   252  			}
   253  		}
   254  	}
   255  
   256  	for snapshot, uris := range snapshotSet {
   257  		// If a modification comes in for the view's go.mod file and the view
   258  		// was never properly initialized, or the view does not have
   259  		// a go.mod file, try to recreate the associated view.
   260  		if modfile := snapshot.View().ModFile(); modfile == "" {
   261  			for _, uri := range uris {
   262  				// Don't rebuild the view until the go.mod is on disk.
   263  				if !snapshot.IsSaved(uri) {
   264  					continue
   265  				}
   266  				fh, err := snapshot.GetFile(ctx, uri)
   267  				if err != nil {
   268  					return err
   269  				}
   270  				switch fh.Kind() {
   271  				case source.Mod:
   272  					newSnapshot, release, err := snapshot.View().Rebuild(ctx)
   273  					releases = append(releases, release)
   274  					if err != nil {
   275  						return err
   276  					}
   277  					// Update the snapshot to the rebuilt one.
   278  					snapshot = newSnapshot
   279  				}
   280  			}
   281  		}
   282  		diagnosticWG.Add(1)
   283  		go func(snapshot source.Snapshot) {
   284  			defer diagnosticWG.Done()
   285  			s.diagnoseSnapshot(snapshot)
   286  		}(snapshot)
   287  	}
   288  
   289  	go func() {
   290  		diagnosticWG.Wait()
   291  		for _, release := range releases {
   292  			release()
   293  		}
   294  	}()
   295  	// After any file modifications, we need to update our watched files,
   296  	// in case something changed. Compute the new set of directories to watch,
   297  	// and if it differs from the current set, send updated registrations.
   298  	if err := s.updateWatchedDirectories(ctx, snapshots); err != nil {
   299  		return err
   300  	}
   301  	return nil
   302  }
   303  
   304  // DiagnosticWorkTitle returns the title of the diagnostic work resulting from a
   305  // file change originating from the given cause.
   306  func DiagnosticWorkTitle(cause ModificationSource) string {
   307  	return fmt.Sprintf("diagnosing %v", cause)
   308  }
   309  
   310  func (s *Server) wasFirstChange(uri span.URI) bool {
   311  	s.changedFilesMu.Lock()
   312  	defer s.changedFilesMu.Unlock()
   313  
   314  	if s.changedFiles == nil {
   315  		s.changedFiles = make(map[span.URI]struct{})
   316  	}
   317  	_, ok := s.changedFiles[uri]
   318  	return !ok
   319  }
   320  
   321  func (s *Server) changedText(ctx context.Context, uri span.URI, changes []protocol.TextDocumentContentChangeEvent) ([]byte, error) {
   322  	if len(changes) == 0 {
   323  		return nil, errors.Errorf("%w: no content changes provided", jsonrpc2.ErrInternal)
   324  	}
   325  
   326  	// Check if the client sent the full content of the file.
   327  	// We accept a full content change even if the server expected incremental changes.
   328  	if len(changes) == 1 && changes[0].Range == nil && changes[0].RangeLength == 0 {
   329  		return []byte(changes[0].Text), nil
   330  	}
   331  	return s.applyIncrementalChanges(ctx, uri, changes)
   332  }
   333  
   334  func (s *Server) applyIncrementalChanges(ctx context.Context, uri span.URI, changes []protocol.TextDocumentContentChangeEvent) ([]byte, error) {
   335  	fh, err := s.session.GetFile(ctx, uri)
   336  	if err != nil {
   337  		return nil, err
   338  	}
   339  	content, err := fh.Read()
   340  	if err != nil {
   341  		return nil, errors.Errorf("%w: file not found (%v)", jsonrpc2.ErrInternal, err)
   342  	}
   343  	for _, change := range changes {
   344  		// Make sure to update column mapper along with the content.
   345  		converter := span.NewContentConverter(uri.Filename(), content)
   346  		m := &protocol.ColumnMapper{
   347  			URI:       uri,
   348  			Converter: converter,
   349  			Content:   content,
   350  		}
   351  		if change.Range == nil {
   352  			return nil, errors.Errorf("%w: unexpected nil range for change", jsonrpc2.ErrInternal)
   353  		}
   354  		spn, err := m.RangeSpan(*change.Range)
   355  		if err != nil {
   356  			return nil, err
   357  		}
   358  		if !spn.HasOffset() {
   359  			return nil, errors.Errorf("%w: invalid range for content change", jsonrpc2.ErrInternal)
   360  		}
   361  		start, end := spn.Start().Offset(), spn.End().Offset()
   362  		if end < start {
   363  			return nil, errors.Errorf("%w: invalid range for content change", jsonrpc2.ErrInternal)
   364  		}
   365  		var buf bytes.Buffer
   366  		buf.Write(content[:start])
   367  		buf.WriteString(change.Text)
   368  		buf.Write(content[end:])
   369  		content = buf.Bytes()
   370  	}
   371  	return content, nil
   372  }
   373  
   374  func changeTypeToFileAction(ct protocol.FileChangeType) source.FileAction {
   375  	switch ct {
   376  	case protocol.Changed:
   377  		return source.Change
   378  	case protocol.Created:
   379  		return source.Create
   380  	case protocol.Deleted:
   381  		return source.Delete
   382  	}
   383  	return source.UnknownFileAction
   384  }