1// Copyright 2025 CUE Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package server
16
17import (
18 "context"
19 "fmt"
20 "path"
21
22 "cuelang.org/go/internal/cueversion"
23 "cuelang.org/go/internal/golangorgx/gopls/progress"
24 "cuelang.org/go/internal/golangorgx/gopls/protocol"
25 "cuelang.org/go/internal/golangorgx/gopls/settings"
26 "cuelang.org/go/internal/golangorgx/tools/event"
27 "cuelang.org/go/internal/golangorgx/tools/jsonrpc2"
28 "cuelang.org/go/internal/lsp/cache"
29)
30
31func validateWorkspaceFolders(folders []protocol.WorkspaceFolder) (map[protocol.WorkspaceFolder]protocol.DocumentURI, error) {
32 withParsedUri := make(map[protocol.WorkspaceFolder]protocol.DocumentURI)
33 for _, folder := range folders {
34 if folder.URI == "" {
35 return nil, fmt.Errorf("empty WorkspaceFolder.URI")
36 }
37 uri, err := protocol.ParseDocumentURI(folder.URI)
38 if err != nil {
39 return nil, fmt.Errorf("invalid WorkspaceFolder.URI: %v", err)
40 }
41 withParsedUri[folder] = uri
42 }
43 return withParsedUri, nil
44}
45
46// Initialize is a request from the editor/client to initialize the
47// workspace. It gets a response. Once the response is sent, the
48// client needs to send an Initialized async message before any work
49// starts.
50//
51// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize
52func (s *server) Initialize(ctx context.Context, params *protocol.ParamInitialize) (*protocol.InitializeResult, error) {
53 ctx, done := event.Start(ctx, "lsp.Server.initialize")
54 defer done()
55
56 if s.state != serverCreated {
57 return nil, fmt.Errorf("%w: initialize called while server in %v state", jsonrpc2.ErrInvalidRequest, s.state)
58 }
59 s.state = serverInitializing
60
61 // TODO(myitcv): need to better understand events, and the mechanisms that
62 // hang off that. At least for now we know that the integration expectation
63 // setup relies on the progress messaging titles to track things being done.
64 s.progress = progress.NewTracker(s.client, params.Capabilities.Window.WorkDoneProgress)
65
66 // Clone the existing (default?) options, and update from the params.
67 options := s.Options().Clone()
68
69 // TODO(myitcv): review and slim down option handling code
70 if err := s.handleOptionResults(ctx, settings.SetOptions(options, params.InitializationOptions)); err != nil {
71 return nil, err
72 }
73 options.ForClientCapabilities(params.ClientInfo, params.Capabilities)
74 s.SetOptions(options)
75
76 folders := params.WorkspaceFolders
77 if len(folders) == 0 && params.RootURI != "" {
78 folders = []protocol.WorkspaceFolder{{
79 URI: string(params.RootURI),
80 Name: path.Base(params.RootURI.Path()),
81 }}
82 }
83
84 validFolders, err := validateWorkspaceFolders(folders)
85 if err != nil {
86 return nil, err
87 }
88 s.eventuallyUseWorkspaceFolders(validFolders)
89
90 var renameOpts any = true
91 if r := params.Capabilities.TextDocument.Rename; r != nil && r.PrepareSupport {
92 renameOpts = protocol.RenameOptions{
93 PrepareProvider: r.PrepareSupport,
94 }
95 }
96
97 return &protocol.InitializeResult{
98 ServerInfo: &protocol.ServerInfo{
99 Name: "cuelsp",
100 Version: cueversion.ModuleVersion(),
101 },
102
103 Capabilities: protocol.ServerCapabilities{
104 CompletionProvider: &protocol.CompletionOptions{
105 TriggerCharacters: []string{"."},
106 },
107 DefinitionProvider: &protocol.Or_ServerCapabilities_definitionProvider{Value: true},
108 DocumentFormattingProvider: &protocol.Or_ServerCapabilities_documentFormattingProvider{Value: true},
109 DocumentSymbolProvider: &protocol.Or_ServerCapabilities_documentSymbolProvider{Value: true},
110 HoverProvider: &protocol.Or_ServerCapabilities_hoverProvider{Value: true},
111 ReferencesProvider: &protocol.Or_ServerCapabilities_referencesProvider{Value: true},
112 RenameProvider: renameOpts,
113 TextDocumentSync: &protocol.TextDocumentSyncOptions{
114 Change: protocol.Incremental,
115 OpenClose: true,
116 Save: &protocol.SaveOptions{
117 IncludeText: false,
118 },
119 },
120
121 Workspace: &protocol.WorkspaceOptions{
122 WorkspaceFolders: &protocol.WorkspaceFolders5Gn{
123 Supported: true,
124 ChangeNotifications: "workspace/didChangeWorkspaceFolders",
125 },
126 },
127 },
128 }, nil
129}
130
131// Initialized is the handler for the async message from the
132// client. The client should send this only after it's received our
133// InitializeResult message.
134//
135// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialized
136func (s *server) Initialized(ctx context.Context, params *protocol.InitializedParams) error {
137 ctx, done := event.Start(ctx, "lsp.Server.initialized")
138 defer done()
139
140 if s.state != serverInitializing {
141 return fmt.Errorf("%w: initialized called while server in %v state", jsonrpc2.ErrInvalidRequest, s.state)
142 }
143 s.state = serverInitialized
144
145 err := s.maybeShowPendingMessages(ctx)
146 if err != nil {
147 return err
148 }
149
150 options := s.Options()
151 if options.ConfigurationSupported && options.DynamicConfigurationSupported {
152 err := s.client.RegisterCapability(ctx, &protocol.RegistrationParams{
153 Registrations: []protocol.Registration{{
154 ID: "workspace/didChangeConfiguration",
155 Method: "workspace/didChangeConfiguration",
156 }},
157 })
158 if err != nil {
159 return err
160 }
161 }
162
163 s.workspace = cache.NewWorkspace(s.cache, s.client, s.debugLog)
164
165 err = s.maybeUseWorkspaceFolders(ctx)
166 // Initialized is a notification, so if there's an error, we show
167 // it rather than return it.
168 if err != nil {
169 s.client.ShowMessage(ctx, &protocol.ShowMessageParams{Type: protocol.Error, Message: err.Error()})
170 }
171 return nil
172}