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 }