github.com/jmigpin/editor@v1.6.0/core/lsproto/manager.go (about) 1 package lsproto 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 11 "github.com/jmigpin/editor/util/iout" 12 "github.com/jmigpin/editor/util/iout/iorw" 13 ) 14 15 // Notes: 16 // - Manager manages LangManagers 17 // - LangManager has a Registration and handles a LangInstance 18 // - Client handles client connection to the lsp server 19 // - ServerWrap, if used, runs the lsp server process 20 type Manager struct { 21 langs []*LangManager 22 msgFn func(string) 23 24 serverWrapW io.Writer // test purposes only 25 } 26 27 func NewManager(msgFn func(string)) *Manager { 28 return &Manager{msgFn: msgFn} 29 } 30 31 //---------- 32 33 func (man *Manager) Error(err error) { 34 man.Message(fmt.Sprintf("error: %v", err)) 35 } 36 37 func (man *Manager) Message(s string) { 38 if man.msgFn != nil { 39 man.msgFn(s) 40 } 41 } 42 43 //---------- 44 45 func (man *Manager) Register(reg *Registration) error { 46 lang := NewLangManager(man, reg) 47 // replace if already exists 48 for i, lang2 := range man.langs { 49 if lang2.Reg.Language == reg.Language { 50 man.langs[i] = lang 51 return nil 52 } 53 } 54 // append new 55 man.langs = append(man.langs, lang) 56 return nil 57 } 58 59 //---------- 60 61 func (man *Manager) LangManager(filename string) (*LangManager, error) { 62 ext := filepath.Ext(filename) 63 for _, lang := range man.langs { 64 for _, ext2 := range lang.Reg.Exts { 65 if ext2 == ext { 66 return lang, nil 67 } 68 } 69 } 70 return nil, fmt.Errorf("no lsproto for file ext: %q", ext) 71 } 72 73 func (man *Manager) langInstanceClient(ctx context.Context, filename string) (*Client, *LangInstance, error) { 74 lang, err := man.LangManager(filename) 75 if err != nil { 76 return nil, nil, err 77 } 78 li, err := lang.instance(ctx) 79 if err != nil { 80 return nil, nil, err 81 } 82 return li.cli, li, nil 83 } 84 85 //---------- 86 87 func (man *Manager) Close() error { 88 count := 0 89 me := &iout.MultiError{} 90 for _, lang := range man.langs { 91 err, ok := lang.Close() 92 if ok { 93 count++ 94 if err != nil { 95 me.Add(err) 96 } else { 97 man.Message(lang.WrapMsg("closed")) 98 } 99 } 100 } 101 if count == 0 { 102 return fmt.Errorf("no instances are running") 103 } 104 return me.Result() 105 } 106 107 //---------- 108 109 func (man *Manager) TextDocumentImplementation(ctx context.Context, filename string, rd iorw.ReaderAt, offset int) (string, *Range, error) { 110 cli, _, err := man.langInstanceClient(ctx, filename) 111 if err != nil { 112 return "", nil, err 113 } 114 115 didCloseFn, err := man.didOpen(ctx, cli, filename, rd) 116 if err != nil { 117 return "", nil, err 118 } 119 defer didCloseFn() 120 121 pos, err := OffsetToPosition(rd, offset) 122 if err != nil { 123 return "", nil, err 124 } 125 126 loc, err := cli.TextDocumentImplementation(ctx, filename, pos) 127 if err != nil { 128 return "", nil, err 129 } 130 131 // target filename 132 filename2, err := UrlToAbsFilename(string(loc.Uri)) 133 if err != nil { 134 return "", nil, err 135 } 136 137 return filename2, loc.Range, nil 138 } 139 140 //---------- 141 142 func (man *Manager) TextDocumentDefinition(ctx context.Context, filename string, rd iorw.ReaderAt, offset int) (string, *Range, error) { 143 cli, _, err := man.langInstanceClient(ctx, filename) 144 if err != nil { 145 return "", nil, err 146 } 147 148 didCloseFn, err := man.didOpen(ctx, cli, filename, rd) 149 if err != nil { 150 return "", nil, err 151 } 152 defer didCloseFn() 153 154 pos, err := OffsetToPosition(rd, offset) 155 if err != nil { 156 return "", nil, err 157 } 158 159 loc, err := cli.TextDocumentDefinition(ctx, filename, pos) 160 if err != nil { 161 return "", nil, err 162 } 163 164 // target filename 165 filename2, err := UrlToAbsFilename(string(loc.Uri)) 166 if err != nil { 167 return "", nil, err 168 } 169 170 return filename2, loc.Range, nil 171 } 172 173 //---------- 174 175 func (man *Manager) TextDocumentCompletion(ctx context.Context, filename string, rd iorw.ReaderAt, offset int) (*CompletionList, error) { 176 cli, _, err := man.langInstanceClient(ctx, filename) 177 if err != nil { 178 return nil, err 179 } 180 181 didCloseFn, err := man.didOpen(ctx, cli, filename, rd) 182 if err != nil { 183 return nil, err 184 } 185 defer didCloseFn() 186 187 pos, err := OffsetToPosition(rd, offset) 188 if err != nil { 189 return nil, err 190 } 191 192 return cli.TextDocumentCompletion(ctx, filename, pos) 193 } 194 195 func (man *Manager) TextDocumentCompletionDetailStrings(ctx context.Context, filename string, rd iorw.ReaderAt, offset int) ([]string, error) { 196 clist, err := man.TextDocumentCompletion(ctx, filename, rd, offset) 197 if err != nil { 198 return nil, err 199 } 200 w := CompletionListToString(clist) 201 return w, nil 202 } 203 204 //---------- 205 206 func (man *Manager) didOpen(ctx context.Context, cli *Client, filename string, rd iorw.ReaderAt) (func(), error) { 207 b, err := iorw.ReadFastFull(rd) 208 if err != nil { 209 return nil, err 210 } 211 if err := cli.TextDocumentDidOpenVersion(ctx, filename, b); err != nil { 212 return nil, err 213 } 214 215 // ISSUE: file1 src is sent to the server (didopen). Assume now that the request that follows (ex: lsprotoCallers) takes too long such that the ctx expires. The usual "defer didclose" will fail since the context is no longer valid. And so the server stays with the version that might have compile errors. The user corrects the errors without asking anything else from the lspserver. Later on, on another file2, asks for the lspserver to assist with something. This could fail since the lspserver still has the file1 cached with errors. 216 // solution: if the didopen was successful, return a func to always run the didClose with defer even if the ctx is no longer valid. 217 didCloseFn := func() { 218 ctx2 := context.Background() // don't use a possible canceled ctx 219 _ = cli.TextDocumentDidClose(ctx2, filename) // best effort, ignore error 220 } 221 return didCloseFn, nil 222 } 223 224 //---------- 225 226 //func (man *Manager) DidSave(ctx context.Context, filename string, text []byte) error { 227 // // no error if there is no lang registered 228 // _, err := man.lang(filename) 229 // if err != nil { 230 // return nil 231 // } 232 // return man.TextDocumentDidSave(ctx, filename, text) 233 //} 234 235 //func (man *Manager) TextDocumentDidSave(ctx context.Context, filename string, text []byte) error { 236 // cli, _, err := man.langInstanceClient(ctx, filename) 237 // if err != nil { 238 // return err 239 // } 240 // return cli.TextDocumentDidSave(ctx, filename, text) 241 //} 242 243 //---------- 244 245 func (man *Manager) SyncText(ctx context.Context, filename string, rd iorw.ReaderAt) error { 246 cli, _, err := man.langInstanceClient(ctx, filename) 247 if err != nil { 248 return err 249 } 250 251 // opening/closing is enough to give the content to the server (using a didsave/didchange would just make it slower since our strategy is to open/close for every change) 252 didCloseFn, err := man.didOpen(ctx, cli, filename, rd) 253 if err != nil { 254 return err 255 } 256 defer didCloseFn() 257 258 return nil 259 } 260 261 //---------- 262 263 func (man *Manager) TextDocumentRename(ctx context.Context, filename string, rd iorw.ReaderAt, offset int, newName string) (*WorkspaceEdit, error) { 264 cli, _, err := man.langInstanceClient(ctx, filename) 265 if err != nil { 266 return nil, err 267 } 268 269 didCloseFn, err := man.didOpen(ctx, cli, filename, rd) 270 if err != nil { 271 return nil, err 272 } 273 defer didCloseFn() 274 275 pos, err := OffsetToPosition(rd, offset) 276 if err != nil { 277 return nil, err 278 } 279 280 return cli.TextDocumentRename(ctx, filename, pos, newName) 281 } 282 283 func (man *Manager) TextDocumentRenameAndPatch(ctx context.Context, filename string, rd iorw.ReaderAt, offset int, newName string, prePatchFn func([]*WorkspaceEditChange) error) ([]*WorkspaceEditChange, error) { 284 285 we, err := man.TextDocumentRename(ctx, filename, rd, offset, newName) 286 if err != nil { 287 return nil, err 288 } 289 290 wecs, err := we.GetChanges() 291 if err != nil { 292 return nil, err 293 } 294 295 if prePatchFn != nil { 296 if err := prePatchFn(wecs); err != nil { 297 return nil, err 298 } 299 } 300 301 // two or more changes to the same file can give trouble (don't using concurrency for this) 302 for _, wec := range wecs { 303 filename := wec.Filename 304 b, err := ioutil.ReadFile(filename) 305 if err != nil { 306 return nil, err 307 } 308 res, err := PatchTextEdits(b, wec.Edits) 309 if err != nil { 310 return nil, err 311 } 312 if err := os.WriteFile(filename, res, 0o644); err != nil { 313 return nil, err 314 } 315 if err := man.SyncText(ctx, filename, rd); err != nil { 316 return nil, err 317 } 318 } 319 320 return wecs, nil 321 } 322 323 //---------- 324 325 func (man *Manager) CallHierarchyCalls(ctx context.Context, filename string, rd iorw.ReaderAt, offset int, typ CallHierarchyCallType) ([]*ManagerCallHierarchyCalls, error) { 326 cli, _, err := man.langInstanceClient(ctx, filename) 327 if err != nil { 328 return nil, err 329 } 330 331 didCloseFn, err := man.didOpen(ctx, cli, filename, rd) 332 if err != nil { 333 return nil, err 334 } 335 defer didCloseFn() 336 337 pos, err := OffsetToPosition(rd, offset) 338 if err != nil { 339 return nil, err 340 } 341 342 items, err := cli.TextDocumentPrepareCallHierarchy(ctx, filename, pos) 343 if err != nil { 344 return nil, err 345 } 346 if len(items) == 0 { 347 return nil, fmt.Errorf("preparecallhierarchy returned no items") 348 } 349 350 res := []*ManagerCallHierarchyCalls{} 351 for _, item := range items { 352 calls, err := cli.CallHierarchyCalls(ctx, typ, item) 353 if err != nil { 354 return nil, err 355 } 356 u := &ManagerCallHierarchyCalls{item, calls} 357 res = append(res, u) 358 } 359 360 return res, nil 361 } 362 363 //---------- 364 365 func (man *Manager) TextDocumentReferences(ctx context.Context, filename string, rd iorw.ReaderAt, offset int) ([]*Location, error) { 366 cli, _, err := man.langInstanceClient(ctx, filename) 367 if err != nil { 368 return nil, err 369 } 370 371 didCloseFn, err := man.didOpen(ctx, cli, filename, rd) 372 if err != nil { 373 return nil, err 374 } 375 defer didCloseFn() 376 377 pos, err := OffsetToPosition(rd, offset) 378 if err != nil { 379 return nil, err 380 } 381 382 return cli.TextDocumentReferences(ctx, filename, pos) 383 }