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 5package lsp 6 7import ( 8 "bytes" 9 "context" 10 "encoding/json" 11 "fmt" 12 "io" 13 "os" 14 "path" 15 "path/filepath" 16 "sync" 17 18 "golang.org/x/tools/internal/event" 19 "golang.org/x/tools/internal/jsonrpc2" 20 "golang.org/x/tools/internal/lsp/debug" 21 "golang.org/x/tools/internal/lsp/protocol" 22 "golang.org/x/tools/internal/lsp/source" 23 "golang.org/x/tools/internal/span" 24 errors "golang.org/x/xerrors" 25) 26 27func (s *Server) initialize(ctx context.Context, params *protocol.ParamInitialize) (*protocol.InitializeResult, error) { 28 s.stateMu.Lock() 29 if s.state >= serverInitializing { 30 defer s.stateMu.Unlock() 31 return nil, errors.Errorf("%w: initialize called while server in %v state", jsonrpc2.ErrInvalidRequest, s.state) 32 } 33 s.state = serverInitializing 34 s.stateMu.Unlock() 35 36 s.clientPID = int(params.ProcessID) 37 s.progress.supportsWorkDoneProgress = params.Capabilities.Window.WorkDoneProgress 38 39 options := s.session.Options() 40 defer func() { s.session.SetOptions(options) }() 41 42 if err := s.handleOptionResults(ctx, source.SetOptions(options, params.InitializationOptions)); err != nil { 43 return nil, err 44 } 45 options.ForClientCapabilities(params.Capabilities) 46 47 folders := params.WorkspaceFolders 48 if len(folders) == 0 { 49 if params.RootURI != "" { 50 folders = []protocol.WorkspaceFolder{{ 51 URI: string(params.RootURI), 52 Name: path.Base(params.RootURI.SpanURI().Filename()), 53 }} 54 } 55 } 56 for _, folder := range folders { 57 uri := span.URIFromURI(folder.URI) 58 if !uri.IsFile() { 59 continue 60 } 61 s.pendingFolders = append(s.pendingFolders, folder) 62 } 63 // gopls only supports URIs with a file:// scheme, so if we have no 64 // workspace folders with a supported scheme, fail to initialize. 65 if len(folders) > 0 && len(s.pendingFolders) == 0 { 66 return nil, fmt.Errorf("unsupported URI schemes: %v (gopls only supports file URIs)", folders) 67 } 68 69 var codeActionProvider interface{} = true 70 if ca := params.Capabilities.TextDocument.CodeAction; len(ca.CodeActionLiteralSupport.CodeActionKind.ValueSet) > 0 { 71 // If the client has specified CodeActionLiteralSupport, 72 // send the code actions we support. 73 // 74 // Using CodeActionOptions is only valid if codeActionLiteralSupport is set. 75 codeActionProvider = &protocol.CodeActionOptions{ 76 CodeActionKinds: s.getSupportedCodeActions(), 77 } 78 } 79 var renameOpts interface{} = true 80 if r := params.Capabilities.TextDocument.Rename; r.PrepareSupport { 81 renameOpts = protocol.RenameOptions{ 82 PrepareProvider: r.PrepareSupport, 83 } 84 } 85 86 goplsVersion, err := json.Marshal(debug.VersionInfo()) 87 if err != nil { 88 return nil, err 89 } 90 91 return &protocol.InitializeResult{ 92 Capabilities: protocol.ServerCapabilities{ 93 CallHierarchyProvider: true, 94 CodeActionProvider: codeActionProvider, 95 CompletionProvider: protocol.CompletionOptions{ 96 TriggerCharacters: []string{"."}, 97 }, 98 DefinitionProvider: true, 99 TypeDefinitionProvider: true, 100 ImplementationProvider: true, 101 DocumentFormattingProvider: true, 102 DocumentSymbolProvider: true, 103 WorkspaceSymbolProvider: true, 104 ExecuteCommandProvider: protocol.ExecuteCommandOptions{ 105 Commands: options.SupportedCommands, 106 }, 107 FoldingRangeProvider: true, 108 HoverProvider: true, 109 DocumentHighlightProvider: true, 110 DocumentLinkProvider: protocol.DocumentLinkOptions{}, 111 ReferencesProvider: true, 112 RenameProvider: renameOpts, 113 SignatureHelpProvider: protocol.SignatureHelpOptions{ 114 TriggerCharacters: []string{"(", ","}, 115 }, 116 TextDocumentSync: &protocol.TextDocumentSyncOptions{ 117 Change: protocol.Incremental, 118 OpenClose: true, 119 Save: protocol.SaveOptions{ 120 IncludeText: false, 121 }, 122 }, 123 Workspace: protocol.WorkspaceGn{ 124 WorkspaceFolders: protocol.WorkspaceFoldersGn{ 125 Supported: true, 126 ChangeNotifications: "workspace/didChangeWorkspaceFolders", 127 }, 128 }, 129 }, 130 ServerInfo: struct { 131 Name string `json:"name"` 132 Version string `json:"version,omitempty"` 133 }{ 134 Name: "gopls", 135 Version: string(goplsVersion), 136 }, 137 }, nil 138} 139 140func (s *Server) initialized(ctx context.Context, params *protocol.InitializedParams) error { 141 s.stateMu.Lock() 142 if s.state >= serverInitialized { 143 defer s.stateMu.Unlock() 144 return errors.Errorf("%w: initialized called while server in %v state", jsonrpc2.ErrInvalidRequest, s.state) 145 } 146 s.state = serverInitialized 147 s.stateMu.Unlock() 148 149 for _, not := range s.notifications { 150 s.client.ShowMessage(ctx, not) 151 } 152 s.notifications = nil 153 154 options := s.session.Options() 155 defer func() { s.session.SetOptions(options) }() 156 157 if err := s.addFolders(ctx, s.pendingFolders); err != nil { 158 return err 159 } 160 s.pendingFolders = nil 161 162 if options.ConfigurationSupported && options.DynamicConfigurationSupported { 163 registrations := []protocol.Registration{ 164 { 165 ID: "workspace/didChangeConfiguration", 166 Method: "workspace/didChangeConfiguration", 167 }, 168 { 169 ID: "workspace/didChangeWorkspaceFolders", 170 Method: "workspace/didChangeWorkspaceFolders", 171 }, 172 } 173 if options.SemanticTokens { 174 registrations = append(registrations, semanticTokenRegistration()) 175 } 176 if err := s.client.RegisterCapability(ctx, &protocol.RegistrationParams{ 177 Registrations: registrations, 178 }); err != nil { 179 return err 180 } 181 } 182 return nil 183} 184 185func (s *Server) addFolders(ctx context.Context, folders []protocol.WorkspaceFolder) error { 186 originalViews := len(s.session.Views()) 187 viewErrors := make(map[span.URI]error) 188 189 var wg sync.WaitGroup 190 if s.session.Options().VerboseWorkDoneProgress { 191 work := s.progress.start(ctx, DiagnosticWorkTitle(FromInitialWorkspaceLoad), "Calculating diagnostics for initial workspace load...", nil, nil) 192 defer func() { 193 go func() { 194 wg.Wait() 195 work.end("Done.") 196 }() 197 }() 198 } 199 // Only one view gets to have a workspace. 200 assignedWorkspace := false 201 var allFoldersWg sync.WaitGroup 202 for _, folder := range folders { 203 uri := span.URIFromURI(folder.URI) 204 // Ignore non-file URIs. 205 if !uri.IsFile() { 206 continue 207 } 208 work := s.progress.start(ctx, "Setting up workspace", "Loading packages...", nil, nil) 209 var workspaceURI span.URI = "" 210 if !assignedWorkspace && s.clientPID != 0 { 211 // For quick-and-dirty testing, set the temp workspace file to 212 // $TMPDIR/gopls-<client PID>.workspace. 213 // 214 // This has a couple limitations: 215 // + If there are multiple workspace roots, only the first one gets 216 // written to this dir (and the client has no way to know precisely 217 // which one). 218 // + If a single client PID spawns multiple gopls sessions, they will 219 // clobber eachother's temp workspace. 220 wsdir := filepath.Join(os.TempDir(), fmt.Sprintf("gopls-%d.workspace", s.clientPID)) 221 workspaceURI = span.URIFromPath(wsdir) 222 assignedWorkspace = true 223 } 224 snapshot, release, err := s.addView(ctx, folder.Name, uri, workspaceURI) 225 if err != nil { 226 viewErrors[uri] = err 227 work.end(fmt.Sprintf("Error loading packages: %s", err)) 228 continue 229 } 230 var swg sync.WaitGroup 231 swg.Add(1) 232 allFoldersWg.Add(1) 233 go func() { 234 defer swg.Done() 235 defer allFoldersWg.Done() 236 snapshot.AwaitInitialized(ctx) 237 work.end("Finished loading packages.") 238 }() 239 240 // Print each view's environment. 241 buf := &bytes.Buffer{} 242 if err := snapshot.WriteEnv(ctx, buf); err != nil { 243 viewErrors[uri] = err 244 continue 245 } 246 event.Log(ctx, buf.String()) 247 248 // Diagnose the newly created view. 249 wg.Add(1) 250 go func() { 251 s.diagnoseDetached(snapshot) 252 swg.Wait() 253 release() 254 wg.Done() 255 }() 256 } 257 258 // Register for file watching notifications, if they are supported. 259 // Wait for all snapshots to be initialized first, since all files might 260 // not yet be known to the snapshots. 261 allFoldersWg.Wait() 262 if err := s.updateWatchedDirectories(ctx); err != nil { 263 event.Error(ctx, "failed to register for file watching notifications", err) 264 } 265 266 if len(viewErrors) > 0 { 267 errMsg := fmt.Sprintf("Error loading workspace folders (expected %v, got %v)\n", len(folders), len(s.session.Views())-originalViews) 268 for uri, err := range viewErrors { 269 errMsg += fmt.Sprintf("failed to load view for %s: %v\n", uri, err) 270 } 271 return s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ 272 Type: protocol.Error, 273 Message: errMsg, 274 }) 275 } 276 return nil 277} 278 279// updateWatchedDirectories compares the current set of directories to watch 280// with the previously registered set of directories. If the set of directories 281// has changed, we unregister and re-register for file watching notifications. 282// updatedSnapshots is the set of snapshots that have been updated. 283func (s *Server) updateWatchedDirectories(ctx context.Context) error { 284 patterns := s.session.FileWatchingGlobPatterns(ctx) 285 286 s.watchedGlobPatternsMu.Lock() 287 defer s.watchedGlobPatternsMu.Unlock() 288 289 // Nothing to do if the set of workspace directories is unchanged. 290 if equalURISet(s.watchedGlobPatterns, patterns) { 291 return nil 292 } 293 294 // If the set of directories to watch has changed, register the updates and 295 // unregister the previously watched directories. This ordering avoids a 296 // period where no files are being watched. Still, if a user makes on-disk 297 // changes before these updates are complete, we may miss them for the new 298 // directories. 299 prevID := s.watchRegistrationCount - 1 300 if err := s.registerWatchedDirectoriesLocked(ctx, patterns); err != nil { 301 return err 302 } 303 if prevID >= 0 { 304 return s.client.UnregisterCapability(ctx, &protocol.UnregistrationParams{ 305 Unregisterations: []protocol.Unregistration{{ 306 ID: watchedFilesCapabilityID(prevID), 307 Method: "workspace/didChangeWatchedFiles", 308 }}, 309 }) 310 } 311 return nil 312} 313 314func watchedFilesCapabilityID(id int) string { 315 return fmt.Sprintf("workspace/didChangeWatchedFiles-%d", id) 316} 317 318func equalURISet(m1, m2 map[string]struct{}) bool { 319 if len(m1) != len(m2) { 320 return false 321 } 322 for k := range m1 { 323 _, ok := m2[k] 324 if !ok { 325 return false 326 } 327 } 328 return true 329} 330 331// registerWatchedDirectoriesLocked sends the workspace/didChangeWatchedFiles 332// registrations to the client and updates s.watchedDirectories. 333func (s *Server) registerWatchedDirectoriesLocked(ctx context.Context, patterns map[string]struct{}) error { 334 if !s.session.Options().DynamicWatchedFilesSupported { 335 return nil 336 } 337 for k := range s.watchedGlobPatterns { 338 delete(s.watchedGlobPatterns, k) 339 } 340 var watchers []protocol.FileSystemWatcher 341 for pattern := range patterns { 342 watchers = append(watchers, protocol.FileSystemWatcher{ 343 GlobPattern: pattern, 344 Kind: float64(protocol.WatchChange + protocol.WatchDelete + protocol.WatchCreate), 345 }) 346 } 347 348 if err := s.client.RegisterCapability(ctx, &protocol.RegistrationParams{ 349 Registrations: []protocol.Registration{{ 350 ID: watchedFilesCapabilityID(s.watchRegistrationCount), 351 Method: "workspace/didChangeWatchedFiles", 352 RegisterOptions: protocol.DidChangeWatchedFilesRegistrationOptions{ 353 Watchers: watchers, 354 }, 355 }}, 356 }); err != nil { 357 return err 358 } 359 s.watchRegistrationCount++ 360 361 for k, v := range patterns { 362 s.watchedGlobPatterns[k] = v 363 } 364 return nil 365} 366 367func (s *Server) fetchConfig(ctx context.Context, name string, folder span.URI, o *source.Options) error { 368 if !s.session.Options().ConfigurationSupported { 369 return nil 370 } 371 configs, err := s.client.Configuration(ctx, &protocol.ParamConfiguration{ 372 ConfigurationParams: protocol.ConfigurationParams{ 373 Items: []protocol.ConfigurationItem{{ 374 ScopeURI: string(folder), 375 Section: "gopls", 376 }}, 377 }, 378 }) 379 if err != nil { 380 return fmt.Errorf("failed to get workspace configuration from client (%s): %v", folder, err) 381 } 382 for _, config := range configs { 383 if err := s.handleOptionResults(ctx, source.SetOptions(o, config)); err != nil { 384 return err 385 } 386 } 387 return nil 388} 389 390func (s *Server) eventuallyShowMessage(ctx context.Context, msg *protocol.ShowMessageParams) error { 391 s.stateMu.Lock() 392 defer s.stateMu.Unlock() 393 if s.state == serverInitialized { 394 return s.client.ShowMessage(ctx, msg) 395 } 396 s.notifications = append(s.notifications, msg) 397 return nil 398} 399 400func (s *Server) handleOptionResults(ctx context.Context, results source.OptionResults) error { 401 for _, result := range results { 402 if result.Error != nil { 403 msg := &protocol.ShowMessageParams{ 404 Type: protocol.Error, 405 Message: result.Error.Error(), 406 } 407 if err := s.eventuallyShowMessage(ctx, msg); err != nil { 408 return err 409 } 410 } 411 switch result.State { 412 case source.OptionUnexpected: 413 msg := &protocol.ShowMessageParams{ 414 Type: protocol.Error, 415 Message: fmt.Sprintf("unexpected gopls setting %q", result.Name), 416 } 417 if err := s.eventuallyShowMessage(ctx, msg); err != nil { 418 return err 419 } 420 case source.OptionDeprecated: 421 msg := fmt.Sprintf("gopls setting %q is deprecated", result.Name) 422 if result.Replacement != "" { 423 msg = fmt.Sprintf("%s, use %q instead", msg, result.Replacement) 424 } 425 if err := s.eventuallyShowMessage(ctx, &protocol.ShowMessageParams{ 426 Type: protocol.Warning, 427 Message: msg, 428 }); err != nil { 429 return err 430 } 431 } 432 } 433 return nil 434} 435 436// beginFileRequest checks preconditions for a file-oriented request and routes 437// it to a snapshot. 438// We don't want to return errors for benign conditions like wrong file type, 439// so callers should do if !ok { return err } rather than if err != nil. 440func (s *Server) beginFileRequest(ctx context.Context, pURI protocol.DocumentURI, expectKind source.FileKind) (source.Snapshot, source.VersionedFileHandle, bool, func(), error) { 441 uri := pURI.SpanURI() 442 if !uri.IsFile() { 443 // Not a file URI. Stop processing the request, but don't return an error. 444 return nil, nil, false, func() {}, nil 445 } 446 view, err := s.session.ViewOf(uri) 447 if err != nil { 448 return nil, nil, false, func() {}, err 449 } 450 snapshot, release := view.Snapshot(ctx) 451 fh, err := snapshot.GetVersionedFile(ctx, uri) 452 if err != nil { 453 release() 454 return nil, nil, false, func() {}, err 455 } 456 if expectKind != source.UnknownKind && fh.Kind() != expectKind { 457 // Wrong kind of file. Nothing to do. 458 release() 459 return nil, nil, false, func() {}, nil 460 } 461 return snapshot, fh, true, release, nil 462} 463 464func (s *Server) shutdown(ctx context.Context) error { 465 s.stateMu.Lock() 466 defer s.stateMu.Unlock() 467 if s.state < serverInitialized { 468 event.Log(ctx, "server shutdown without initialization") 469 } 470 if s.state != serverShutDown { 471 // drop all the active views 472 s.session.Shutdown(ctx) 473 s.state = serverShutDown 474 } 475 return nil 476} 477 478func (s *Server) exit(ctx context.Context) error { 479 s.stateMu.Lock() 480 defer s.stateMu.Unlock() 481 482 // TODO: We need a better way to find the conn close method. 483 s.client.(io.Closer).Close() 484 485 if s.state != serverShutDown { 486 // TODO: We should be able to do better than this. 487 os.Exit(1) 488 } 489 // we don't terminate the process on a normal exit, we just allow it to 490 // close naturally if needed after the connection is closed. 491 return nil 492} 493