cuelang.org/go@v0.13.0/internal/golangorgx/gopls/server/general.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  // This file defines server methods related to initialization,
     8  // options, shutdown, and exit.
     9  
    10  import (
    11  	"context"
    12  	"encoding/json"
    13  	"fmt"
    14  	"os"
    15  	"path"
    16  	"path/filepath"
    17  	"sort"
    18  	"strings"
    19  	"sync"
    20  
    21  	"cuelang.org/go/internal/golangorgx/gopls/cache"
    22  	"cuelang.org/go/internal/golangorgx/gopls/debug"
    23  	"cuelang.org/go/internal/golangorgx/gopls/file"
    24  	"cuelang.org/go/internal/golangorgx/gopls/protocol"
    25  	"cuelang.org/go/internal/golangorgx/gopls/settings"
    26  	"cuelang.org/go/internal/golangorgx/gopls/util/maps"
    27  	"cuelang.org/go/internal/golangorgx/tools/event"
    28  	"cuelang.org/go/internal/golangorgx/tools/jsonrpc2"
    29  )
    30  
    31  func (s *server) Initialize(ctx context.Context, params *protocol.ParamInitialize) (*protocol.InitializeResult, error) {
    32  	ctx, done := event.Start(ctx, "lsp.Server.initialize")
    33  	defer done()
    34  
    35  	s.stateMu.Lock()
    36  	if s.state >= serverInitializing {
    37  		defer s.stateMu.Unlock()
    38  		return nil, fmt.Errorf("%w: initialize called while server in %v state", jsonrpc2.ErrInvalidRequest, s.state)
    39  	}
    40  	s.state = serverInitializing
    41  	s.stateMu.Unlock()
    42  
    43  	// Create a temp dir using the server PID. For uniqueness, use the server
    44  	// PID rather than params.ProcessID (the client pid).
    45  	pid := os.Getpid()
    46  	s.tempDir = filepath.Join(os.TempDir(), fmt.Sprintf("cue-lsp-%d.%s", pid, s.session.ID()))
    47  	err := os.Mkdir(s.tempDir, 0700)
    48  	if err != nil {
    49  		// MkdirTemp could fail due to permissions issues. This is a problem with
    50  		// the user's environment, but should not block gopls otherwise behaving.
    51  		// All usage of s.tempDir should be predicated on having a non-empty
    52  		// s.tempDir.
    53  		event.Error(ctx, "creating temp dir", err)
    54  		s.tempDir = ""
    55  	}
    56  
    57  	// TODO(myitcv): need to better understand events, and the mechanisms that
    58  	// hang off that. At least for now we know that the integration expectation
    59  	// setup relies on the progress messaging titles to track things being done.
    60  	s.progress.SetSupportsWorkDoneProgress(params.Capabilities.Window.WorkDoneProgress)
    61  
    62  	// Clone the existing (default?) options, and update from the params. Defer setting
    63  	// the options for readability reasons (rather than having that call "lost" below any
    64  	// params-options related code below, it's easier to read locally here).
    65  	options := s.Options().Clone()
    66  	defer s.SetOptions(options)
    67  
    68  	// TODO(myitcv): review and slim down option handling code
    69  	if err := s.handleOptionResults(ctx, settings.SetOptions(options, params.InitializationOptions)); err != nil {
    70  		return nil, err
    71  	}
    72  	options.ForClientCapabilities(params.ClientInfo, params.Capabilities)
    73  
    74  	// An LSP WorkspaceFolder corresponds to a Session View. For now, we only
    75  	// want to support a single WorkspaceFolder, to avoid any complex logic of
    76  	// View handling. Therefore, error in case we get anything other than a
    77  	// single WorkspaceFolder during Initialize. Also error when handling
    78  	// didChangeWorkspaceFolders in case that state changes. We can then
    79  	// prioritise work to support different clients etc based on bug reports.
    80  	//
    81  	// Ensure this logic is consistent with [server.DidChangeWorkspaceFolders].
    82  	folders := params.WorkspaceFolders
    83  	if len(folders) == 0 {
    84  		if params.RootURI != "" {
    85  			folders = []protocol.WorkspaceFolder{{
    86  				URI:  string(params.RootURI),
    87  				Name: path.Base(params.RootURI.Path()),
    88  			}}
    89  		}
    90  	}
    91  
    92  	if l := len(folders); l != 1 {
    93  		return nil, fmt.Errorf("got %d WorkspaceFolders; expected 1", l)
    94  	}
    95  
    96  	for _, folder := range folders {
    97  		if folder.URI == "" {
    98  			return nil, fmt.Errorf("empty WorkspaceFolder.URI")
    99  		}
   100  		if _, err := protocol.ParseDocumentURI(folder.URI); err != nil {
   101  			return nil, fmt.Errorf("invalid WorkspaceFolder.URI: %v", err)
   102  		}
   103  		s.pendingFolders = append(s.pendingFolders, folder)
   104  	}
   105  
   106  	// TODO(myitcv): later we should leverage the existing mechanism for getting
   107  	// version information in cmd/cue. For now, let's just work around that.
   108  	versionInfo := debug.VersionInfo()
   109  	cueLspVersion, err := json.Marshal(versionInfo)
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  
   114  	return &protocol.InitializeResult{
   115  		Capabilities: protocol.ServerCapabilities{
   116  			DocumentFormattingProvider: &protocol.Or_ServerCapabilities_documentFormattingProvider{Value: true},
   117  			TextDocumentSync: &protocol.TextDocumentSyncOptions{
   118  				Change:    protocol.Incremental,
   119  				OpenClose: true,
   120  				Save: &protocol.SaveOptions{
   121  					IncludeText: false,
   122  				},
   123  			},
   124  
   125  			// Even though we don't support this for now, it's worth having the
   126  			// feature enabled to that users run into the error of it not being
   127  			// supported. It's a more obvious signal that the feature isn't
   128  			// supported (yet), and a clear action to raise an issue if this is
   129  			// something that they need.
   130  			Workspace: &protocol.WorkspaceOptions{
   131  				WorkspaceFolders: &protocol.WorkspaceFolders5Gn{
   132  					Supported:           true,
   133  					ChangeNotifications: "workspace/didChangeWorkspaceFolders",
   134  				},
   135  			},
   136  		},
   137  		ServerInfo: &protocol.ServerInfo{
   138  			Name:    "cue lsp",
   139  			Version: string(cueLspVersion),
   140  		},
   141  	}, nil
   142  }
   143  
   144  func (s *server) Initialized(ctx context.Context, params *protocol.InitializedParams) error {
   145  	ctx, done := event.Start(ctx, "lsp.Server.initialized")
   146  	defer done()
   147  
   148  	s.stateMu.Lock()
   149  	if s.state >= serverInitialized {
   150  		defer s.stateMu.Unlock()
   151  		return fmt.Errorf("%w: initialized called while server in %v state", jsonrpc2.ErrInvalidRequest, s.state)
   152  	}
   153  	s.state = serverInitialized
   154  	s.stateMu.Unlock()
   155  
   156  	// TODO(myitcv): I _think_ that even in the case that we are using a daemon instance,
   157  	// then the pid that the client sees is that of the ultimate LSP server. Which is correct.
   158  	// Even though the client is connected through a forwarder, this is a dumb process that
   159  	// shouldn't affect behaviour in any way. This TODO captures ensuring that is the case.
   160  	// If so, this TODO can be removed.
   161  	pid := os.Getpid()
   162  	event.Log(ctx, fmt.Sprintf("cue lsp server pid: %d", pid))
   163  
   164  	for _, not := range s.notifications {
   165  		s.client.ShowMessage(ctx, not)
   166  	}
   167  	s.notifications = nil
   168  
   169  	// Now create views for the pending folders
   170  	s.addFolders(ctx, s.pendingFolders)
   171  	s.pendingFolders = nil
   172  
   173  	return nil
   174  }
   175  
   176  // addFolders gets called from Initialized to add the specified list of
   177  // WorkspaceFolders to the session. There is no sense in returning an error
   178  // here, because Initialized is a notification. As such, we report any errors
   179  // in the loading process as shown messages to the end user.
   180  //
   181  // If we start to call addFolders from elsewhere, i.e. at another point during
   182  // the lifetime of 'cue lsp', we will need to _very_ carefully consider the
   183  // assumptions that are otherwise made in the code below.
   184  //
   185  // Precondition: each folder in folders must have a valid URI.
   186  func (s *server) addFolders(ctx context.Context, folders []protocol.WorkspaceFolder) {
   187  	originalViews := len(s.session.Views())
   188  	viewErrors := make(map[protocol.URI]error)
   189  
   190  	var ndiagnose sync.WaitGroup // number of unfinished diagnose calls
   191  
   192  	// Do not remove this progress notification. It is a key part of integration
   193  	// tests knowing when 'cue lsp' is "ready" post the Initialized-initiated
   194  	// workspace load.
   195  	if s.Options().VerboseWorkDoneProgress {
   196  		work := s.progress.Start(ctx, DiagnosticWorkTitle(FromInitialWorkspaceLoad), "Calculating diagnostics for initial workspace load...", nil, nil)
   197  		defer func() {
   198  			go func() {
   199  				ndiagnose.Wait()
   200  				work.End(ctx, "Done.")
   201  			}()
   202  		}()
   203  	}
   204  
   205  	// Only one view gets to have a workspace.
   206  	var nsnapshots sync.WaitGroup // number of unfinished snapshot initializations
   207  	for _, folder := range folders {
   208  		uri, err := protocol.ParseDocumentURI(folder.URI)
   209  		if err != nil {
   210  			// Precondition on folders having valid URI violated
   211  			panic(err)
   212  		}
   213  
   214  		work := s.progress.Start(ctx, "Setting up workspace", "Loading packages...", nil, nil)
   215  		snapshot, release, err := s.addView(ctx, folder.Name, uri)
   216  		if err != nil {
   217  			viewErrors[folder.URI] = err
   218  			work.End(ctx, fmt.Sprintf("Error loading packages: %s", err))
   219  			continue
   220  		}
   221  		// Inv: release() must be called once.
   222  
   223  		// Initialize snapshot asynchronously.
   224  		initialized := make(chan struct{})
   225  		nsnapshots.Add(1)
   226  		go func() {
   227  			snapshot.AwaitInitialized(ctx)
   228  			work.End(ctx, "Finished loading packages.")
   229  			nsnapshots.Done()
   230  			close(initialized) // signal
   231  		}()
   232  
   233  		// Diagnose the newly created view asynchronously.
   234  		ndiagnose.Add(1)
   235  		go func() {
   236  			s.diagnoseSnapshot(snapshot, nil, 0)
   237  			<-initialized
   238  			release()
   239  			ndiagnose.Done()
   240  		}()
   241  	}
   242  
   243  	// Wait for snapshots to be initialized so that all files are known.
   244  	// (We don't need to wait for diagnosis to finish.)
   245  	nsnapshots.Wait()
   246  
   247  	// Register for file watching notifications, if they are supported.
   248  	if err := s.updateWatchedDirectories(ctx); err != nil {
   249  		event.Error(ctx, "failed to register for file watching notifications", err)
   250  	}
   251  
   252  	// Report any errors using the protocol.
   253  	if len(viewErrors) > 0 {
   254  		errMsg := fmt.Sprintf("Error loading workspace folders (expected %v, got %v)\n", len(folders), len(s.session.Views())-originalViews)
   255  		for uri, err := range viewErrors {
   256  			errMsg += fmt.Sprintf("failed to load view for %s: %v\n", uri, err)
   257  		}
   258  		showMessage(ctx, s.client, protocol.Error, errMsg)
   259  	}
   260  }
   261  
   262  // updateWatchedDirectories compares the current set of directories to watch
   263  // with the previously registered set of directories. If the set of directories
   264  // has changed, we unregister and re-register for file watching notifications.
   265  // updatedSnapshots is the set of snapshots that have been updated.
   266  func (s *server) updateWatchedDirectories(ctx context.Context) error {
   267  	patterns := s.session.FileWatchingGlobPatterns(ctx)
   268  
   269  	s.watchedGlobPatternsMu.Lock()
   270  	defer s.watchedGlobPatternsMu.Unlock()
   271  
   272  	// Nothing to do if the set of workspace directories is unchanged.
   273  	if maps.SameKeys(s.watchedGlobPatterns, patterns) {
   274  		return nil
   275  	}
   276  
   277  	// If the set of directories to watch has changed, register the updates and
   278  	// unregister the previously watched directories. This ordering avoids a
   279  	// period where no files are being watched. Still, if a user makes on-disk
   280  	// changes before these updates are complete, we may miss them for the new
   281  	// directories.
   282  	prevID := s.watchRegistrationCount - 1
   283  	if err := s.registerWatchedDirectoriesLocked(ctx, patterns); err != nil {
   284  		return err
   285  	}
   286  	if prevID >= 0 {
   287  		return s.client.UnregisterCapability(ctx, &protocol.UnregistrationParams{
   288  			Unregisterations: []protocol.Unregistration{{
   289  				ID:     watchedFilesCapabilityID(prevID),
   290  				Method: "workspace/didChangeWatchedFiles",
   291  			}},
   292  		})
   293  	}
   294  	return nil
   295  }
   296  
   297  func watchedFilesCapabilityID(id int) string {
   298  	return fmt.Sprintf("workspace/didChangeWatchedFiles-%d", id)
   299  }
   300  
   301  // registerWatchedDirectoriesLocked sends the workspace/didChangeWatchedFiles
   302  // registrations to the client and updates s.watchedDirectories.
   303  // The caller must not subsequently mutate patterns.
   304  func (s *server) registerWatchedDirectoriesLocked(ctx context.Context, patterns map[protocol.RelativePattern]unit) error {
   305  	if !s.Options().DynamicWatchedFilesSupported {
   306  		return nil
   307  	}
   308  
   309  	supportsRelativePatterns := s.Options().RelativePatternsSupported
   310  
   311  	s.watchedGlobPatterns = patterns
   312  	watchers := make([]protocol.FileSystemWatcher, 0, len(patterns)) // must be a slice
   313  	val := protocol.WatchChange | protocol.WatchDelete | protocol.WatchCreate
   314  	for pattern := range patterns {
   315  		var value any
   316  		if supportsRelativePatterns && pattern.BaseURI != "" {
   317  			value = pattern
   318  		} else {
   319  			p := pattern.Pattern
   320  			if pattern.BaseURI != "" {
   321  				p = path.Join(filepath.ToSlash(pattern.BaseURI.Path()), p)
   322  			}
   323  			value = p
   324  		}
   325  		watchers = append(watchers, protocol.FileSystemWatcher{
   326  			GlobPattern: protocol.GlobPattern{Value: value},
   327  			Kind:        &val,
   328  		})
   329  	}
   330  
   331  	if err := s.client.RegisterCapability(ctx, &protocol.RegistrationParams{
   332  		Registrations: []protocol.Registration{{
   333  			ID:     watchedFilesCapabilityID(s.watchRegistrationCount),
   334  			Method: "workspace/didChangeWatchedFiles",
   335  			RegisterOptions: protocol.DidChangeWatchedFilesRegistrationOptions{
   336  				Watchers: watchers,
   337  			},
   338  		}},
   339  	}); err != nil {
   340  		return err
   341  	}
   342  	s.watchRegistrationCount++
   343  	return nil
   344  }
   345  
   346  // Options returns the current server options.
   347  //
   348  // The caller must not modify the result.
   349  func (s *server) Options() *settings.Options {
   350  	s.optionsMu.Lock()
   351  	defer s.optionsMu.Unlock()
   352  	return s.options
   353  }
   354  
   355  // SetOptions sets the current server options.
   356  //
   357  // The caller must not subsequently modify the options.
   358  func (s *server) SetOptions(opts *settings.Options) {
   359  	s.optionsMu.Lock()
   360  	defer s.optionsMu.Unlock()
   361  	s.options = opts
   362  }
   363  
   364  func (s *server) newFolder(ctx context.Context, folder protocol.DocumentURI, name string) (*cache.Folder, error) {
   365  	opts := s.Options()
   366  	if opts.ConfigurationSupported {
   367  		scope := string(folder)
   368  		configs, err := s.client.Configuration(ctx, &protocol.ParamConfiguration{
   369  			Items: []protocol.ConfigurationItem{{
   370  				ScopeURI: &scope,
   371  				Section:  "cue lsp",
   372  			}},
   373  		},
   374  		)
   375  		if err != nil {
   376  			return nil, fmt.Errorf("failed to get workspace configuration from client (%s): %v", folder, err)
   377  		}
   378  
   379  		opts = opts.Clone()
   380  		for _, config := range configs {
   381  			if err := s.handleOptionResults(ctx, settings.SetOptions(opts, config)); err != nil {
   382  				return nil, err
   383  			}
   384  		}
   385  	}
   386  
   387  	return &cache.Folder{
   388  		Dir:     folder,
   389  		Name:    name,
   390  		Options: opts,
   391  	}, nil
   392  }
   393  
   394  // fetchFolderOptions makes a workspace/configuration request for the given
   395  // folder, and populates options with the result.
   396  //
   397  // If folder is "", fetchFolderOptions makes an unscoped request.
   398  func (s *server) fetchFolderOptions(ctx context.Context, folder protocol.DocumentURI) (*settings.Options, error) {
   399  	opts := s.Options()
   400  	if !opts.ConfigurationSupported {
   401  		return opts, nil
   402  	}
   403  	var scopeURI *string
   404  	if folder != "" {
   405  		scope := string(folder)
   406  		scopeURI = &scope
   407  	}
   408  	configs, err := s.client.Configuration(ctx, &protocol.ParamConfiguration{
   409  		Items: []protocol.ConfigurationItem{{
   410  			ScopeURI: scopeURI,
   411  			Section:  "gopls",
   412  		}},
   413  	},
   414  	)
   415  	if err != nil {
   416  		return nil, fmt.Errorf("failed to get workspace configuration from client (%s): %v", folder, err)
   417  	}
   418  
   419  	opts = opts.Clone()
   420  	for _, config := range configs {
   421  		if err := s.handleOptionResults(ctx, settings.SetOptions(opts, config)); err != nil {
   422  			return nil, err
   423  		}
   424  	}
   425  	return opts, nil
   426  }
   427  
   428  func (s *server) eventuallyShowMessage(ctx context.Context, msg *protocol.ShowMessageParams) error {
   429  	s.stateMu.Lock()
   430  	defer s.stateMu.Unlock()
   431  	if s.state == serverInitialized {
   432  		return s.client.ShowMessage(ctx, msg)
   433  	}
   434  	s.notifications = append(s.notifications, msg)
   435  	return nil
   436  }
   437  
   438  func (s *server) handleOptionResults(ctx context.Context, results settings.OptionResults) error {
   439  	var warnings, errors []string
   440  	for _, result := range results {
   441  		switch result.Error.(type) {
   442  		case nil:
   443  			// nothing to do
   444  		case *settings.SoftError:
   445  			warnings = append(warnings, result.Error.Error())
   446  		default:
   447  			errors = append(errors, result.Error.Error())
   448  		}
   449  	}
   450  
   451  	// Sort messages, but put errors first.
   452  	//
   453  	// Having stable content for the message allows clients to de-duplicate. This
   454  	// matters because we may send duplicate warnings for clients that support
   455  	// dynamic configuration: one for the initial settings, and then more for the
   456  	// individual viewsettings.
   457  	var msgs []string
   458  	msgType := protocol.Warning
   459  	if len(errors) > 0 {
   460  		msgType = protocol.Error
   461  		sort.Strings(errors)
   462  		msgs = append(msgs, errors...)
   463  	}
   464  	if len(warnings) > 0 {
   465  		sort.Strings(warnings)
   466  		msgs = append(msgs, warnings...)
   467  	}
   468  
   469  	if len(msgs) > 0 {
   470  		// Settings
   471  		combined := "Invalid settings: " + strings.Join(msgs, "; ")
   472  		params := &protocol.ShowMessageParams{
   473  			Type:    msgType,
   474  			Message: combined,
   475  		}
   476  		return s.eventuallyShowMessage(ctx, params)
   477  	}
   478  
   479  	return nil
   480  }
   481  
   482  // fileOf returns the file for a given URI and its snapshot.
   483  // On success, the returned function must be called to release the snapshot.
   484  func (s *server) fileOf(ctx context.Context, uri protocol.DocumentURI) (file.Handle, *cache.Snapshot, func(), error) {
   485  	snapshot, release, err := s.session.SnapshotOf(ctx, uri)
   486  	if err != nil {
   487  		return nil, nil, nil, err
   488  	}
   489  	fh, err := snapshot.ReadFile(ctx, uri)
   490  	if err != nil {
   491  		release()
   492  		return nil, nil, nil, err
   493  	}
   494  	return fh, snapshot, release, nil
   495  }
   496  
   497  // shutdown implements the 'shutdown' LSP handler. It releases resources
   498  // associated with the server and waits for all ongoing work to complete.
   499  func (s *server) Shutdown(ctx context.Context) error {
   500  	ctx, done := event.Start(ctx, "lsp.Server.shutdown")
   501  	defer done()
   502  
   503  	s.stateMu.Lock()
   504  	defer s.stateMu.Unlock()
   505  	if s.state < serverInitialized {
   506  		event.Log(ctx, "server shutdown without initialization")
   507  	}
   508  	if s.state != serverShutDown {
   509  		// drop all the active views
   510  		s.session.Shutdown(ctx)
   511  		s.state = serverShutDown
   512  		if s.tempDir != "" {
   513  			if err := os.RemoveAll(s.tempDir); err != nil {
   514  				event.Error(ctx, "removing temp dir", err)
   515  			}
   516  		}
   517  	}
   518  	return nil
   519  }
   520  
   521  func (s *server) Exit(ctx context.Context) error {
   522  	ctx, done := event.Start(ctx, "lsp.Server.exit")
   523  	defer done()
   524  
   525  	s.stateMu.Lock()
   526  	defer s.stateMu.Unlock()
   527  
   528  	s.client.Close()
   529  
   530  	if s.state != serverShutDown {
   531  		// TODO: We should be able to do better than this.
   532  		os.Exit(1)
   533  	}
   534  	// We don't terminate the process on a normal exit, we just allow it to
   535  	// close naturally if needed after the connection is closed.
   536  	return nil
   537  }