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  }