github.com/jmigpin/editor@v1.6.0/core/lsproto/client.go (about)

     1  package lsproto
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"net"
     9  	"net/rpc"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/jmigpin/editor/util/ctxutil"
    14  	"github.com/jmigpin/editor/util/iout"
    15  	"github.com/jmigpin/editor/util/iout/iorw"
    16  )
    17  
    18  type Client struct {
    19  	rcli         *rpc.Client
    20  	li           *LangInstance
    21  	readLoopWait sync.WaitGroup
    22  
    23  	lock struct {
    24  		sync.Mutex
    25  		fversions map[string]int
    26  		//folders   []*WorkspaceFolder
    27  	}
    28  
    29  	serverCapabilities struct {
    30  		workspace struct {
    31  			folders bool
    32  			symbol  bool
    33  		}
    34  		rename bool
    35  	}
    36  }
    37  
    38  //----------
    39  
    40  func NewClientTCP(ctx context.Context, addr string, li *LangInstance) (*Client, error) {
    41  	dialer := net.Dialer{Timeout: 5 * time.Second}
    42  	conn, err := dialer.DialContext(ctx, "tcp", addr)
    43  	if err != nil {
    44  		return nil, err
    45  	}
    46  	cli := NewClientIO(ctx, conn, li)
    47  	return cli, nil
    48  }
    49  
    50  //----------
    51  
    52  func NewClientIO(ctx context.Context, rwc io.ReadWriteCloser, li *LangInstance) *Client {
    53  	cli := &Client{li: li}
    54  	cli.lock.fversions = map[string]int{}
    55  
    56  	cc := NewJsonCodec(rwc)
    57  	cc.OnNotificationMessage = cli.onNotificationMessage
    58  	cc.OnUnexpectedServerReply = cli.onUnexpectedServerReply
    59  
    60  	cli.rcli = rpc.NewClientWithCodec(cc)
    61  
    62  	// wait for the codec readloop
    63  	cli.readLoopWait.Add(1)
    64  	go func() {
    65  		defer cli.readLoopWait.Done()
    66  		if err := cc.ReadLoop(); err != nil {
    67  			cli.li.lang.PrintWrapError(err)
    68  			cli.li.cancelCtx()
    69  		}
    70  	}()
    71  
    72  	// close when ctx is done
    73  	go func() {
    74  		select {
    75  		case <-ctx.Done():
    76  			if err := cli.sendClose(); err != nil {
    77  				// Commented: best effort, ignore errors
    78  				//cli.li.lang.PrintWrapError(err)
    79  			}
    80  			if err := rwc.Close(); err != nil {
    81  				cli.li.lang.PrintWrapError(err)
    82  			}
    83  		}
    84  	}()
    85  
    86  	return cli
    87  }
    88  
    89  //----------
    90  
    91  func (cli *Client) Wait() error {
    92  	cli.readLoopWait.Wait()
    93  	return nil
    94  }
    95  
    96  func (cli *Client) sendClose() error {
    97  	me := iout.MultiError{}
    98  	if err := cli.ShutdownRequest(); err != nil {
    99  		me.Add(err)
   100  	} else {
   101  		me.Add(cli.ExitNotification())
   102  	}
   103  	return me.Result()
   104  }
   105  
   106  //----------
   107  
   108  func (cli *Client) Call(ctx context.Context, method string, args, reply interface{}) error {
   109  	lspResp := &Response{}
   110  	fn := func() error {
   111  		return cli.rcli.Call(method, args, lspResp)
   112  	}
   113  	lateFn := func(err error) {
   114  		if err != nil {
   115  			err = fmt.Errorf("call late: %w", err)
   116  			cli.li.lang.PrintWrapError(err)
   117  		}
   118  	}
   119  	err := ctxutil.Call(ctx, method, fn, lateFn)
   120  	if err != nil {
   121  		err = fmt.Errorf("call: %w", err)
   122  		return cli.li.lang.WrapError(err)
   123  	}
   124  
   125  	// not expecting a reply
   126  	if _, ok := noreplyMethod(method); ok {
   127  		return nil
   128  	}
   129  
   130  	// soft error (rpc data with error content)
   131  	if lspResp.Error != nil {
   132  		return cli.li.lang.WrapError(lspResp.Error)
   133  	}
   134  
   135  	// decode result
   136  	return decodeJsonRaw(lspResp.Result, reply)
   137  }
   138  
   139  //----------
   140  
   141  func (cli *Client) onNotificationMessage(msg *NotificationMessage) {
   142  	// Msgs like:
   143  	// - a notification was sent to the srv, not expecting a reply, but it receives one because it was an error (has id)
   144  	// {"error":{"code":-32601,"message":"method not found"},"id":2,"jsonrpc":"2.0"}
   145  
   146  	//logJson("notification <--: ", msg)
   147  }
   148  
   149  func (cli *Client) onUnexpectedServerReply(resp *Response) {
   150  	if resp.Error != nil {
   151  		// json-rpc error codes: https://www.jsonrpc.org/specification
   152  		report := false
   153  		switch resp.Error.Code {
   154  		case -32601: // method not found
   155  			report = true
   156  		case -32602: // invalid params
   157  			report = true
   158  
   159  			//case -32603: // internal error
   160  			//report = true
   161  		}
   162  		if report {
   163  			err := fmt.Errorf("id=%v, code=%v, msg=%q, data=%v", resp.Id, resp.Error.Code, resp.Error.Message, resp.Error.Data)
   164  			cli.li.lang.PrintWrapError(err)
   165  		}
   166  	}
   167  }
   168  
   169  //----------
   170  
   171  func (cli *Client) Initialize(ctx context.Context) error {
   172  	//opt, err := cli.initializeParams()
   173  	//if err != nil {
   174  	//	return err
   175  	//}
   176  	opt := json.RawMessage("{}")
   177  	logJson("opt -->: ", opt)
   178  
   179  	serverCapabilities := (interface{})(nil)
   180  	if err := cli.Call(ctx, "initialize", opt, &serverCapabilities); err != nil {
   181  		return err
   182  	}
   183  	logJson("initialize <--: ", serverCapabilities)
   184  
   185  	cli.readServerCapabilities(serverCapabilities)
   186  
   187  	// send "initialized" (gopls: "no views" error without this)
   188  	opt2 := json.RawMessage("{}")
   189  	return cli.Call(ctx, "noreply:initialized", opt2, nil)
   190  }
   191  
   192  //func (cli *Client) initializeParams() (json.RawMessage, error) {
   193  //	rootUri, err := cli.rootUri()
   194  //	if err != nil {
   195  //		return nil, err
   196  //	}
   197  //	_ = rootUri
   198  
   199  //	// workspace folders
   200  //	cli.folders = []*WorkspaceFolder{{Uri: rootUri}}
   201  //	foldersBytes, err := encodeJson(cli.folders)
   202  //	if err != nil {
   203  //		return nil, err
   204  //	}
   205  
   206  //	// other capabilities
   207  //	//"capabilities":{
   208  //	//	"workspace":{
   209  //	//		"configuration":true,
   210  //	//		"workspaceFolders":true
   211  //	//	},
   212  //	//	"textDocument":{
   213  //	//		"publishDiagnostics":{
   214  //	//			"relatedInformation":true
   215  //	//		}
   216  //	//	}
   217  //	//}
   218  
   219  //	raw := json.RawMessage("{" +
   220  //		// TODO: gopls is not allowing rooturi=null at the moment...
   221  //		fmt.Sprintf("%q:%q", "rootUri", rootUri) + "," +
   222  //		// set workspace folders to use the later as "remove" value
   223  //		fmt.Sprintf("%q:%s", "workspaceFolders", foldersBytes) +
   224  //		"}")
   225  //	return raw, nil
   226  //}
   227  
   228  //func (cli *Client) rootUri() (DocumentUri, error) {
   229  //	// using a non-existent dir to prevent an lsp server to start scanning the user disk doesn't work well (ex: gopls gives "no views in the session" after the cache is gone)
   230  //	// use initial request file
   231  //	dir := filepath.Dir(cli.li.lang.InstanceReqFilename)
   232  //	rootUrl, err := absFilenameToUrl(dir)
   233  //	if err != nil {
   234  //		return "", err
   235  //	}
   236  //	return DocumentUri(rootUrl), nil
   237  //}
   238  
   239  func (cli *Client) readServerCapabilities(caps interface{}) {
   240  	path := "capabilities.workspace.workspaceFolders.supported"
   241  	v, err := JsonGetPath(caps, path)
   242  	if err == nil {
   243  		if b, ok := v.(bool); ok && b == true {
   244  			cli.serverCapabilities.workspace.folders = true
   245  		}
   246  	}
   247  
   248  	path = "capabilities.workspaceSymbolProvider"
   249  	v, err = JsonGetPath(caps, path)
   250  	if err == nil {
   251  		if b, ok := v.(bool); ok && b == true {
   252  			cli.serverCapabilities.workspace.symbol = true
   253  		}
   254  	}
   255  
   256  	path = "capabilities.renameProvider"
   257  	v, err = JsonGetPath(caps, path)
   258  	if err == nil {
   259  		if b, ok := v.(bool); ok && b == true {
   260  			cli.serverCapabilities.rename = true
   261  		}
   262  	}
   263  }
   264  
   265  //----------
   266  
   267  func (cli *Client) ShutdownRequest() error {
   268  	// https://microsoft.github.io/language-server-protocol/specification#shutdown
   269  
   270  	// TODO: shutdown request should expect a reply
   271  	// * clangd is sending a reply (ok)
   272  	// * gopls is not sending a reply (NOT OK)
   273  
   274  	// best effort, impose timeout
   275  	ctx := context.Background()
   276  	ctx2, cancel := context.WithTimeout(ctx, 1000*time.Millisecond)
   277  	defer cancel()
   278  	ctx = ctx2
   279  
   280  	err := cli.Call(ctx, "shutdown", nil, nil)
   281  	return err
   282  }
   283  
   284  func (cli *Client) ExitNotification() error {
   285  	// https://microsoft.github.io/language-server-protocol/specification#exit
   286  
   287  	// no ctx timeout needed, it's not expecting a reply
   288  	ctx := context.Background()
   289  	err := cli.Call(ctx, "noreply:exit", nil, nil)
   290  	return err
   291  }
   292  
   293  //----------
   294  
   295  func (cli *Client) TextDocumentDidOpen(ctx context.Context, filename, text string, version int) error {
   296  	// https://microsoft.github.io/language-server-protocol/specification#textDocument_didOpen
   297  
   298  	opt := &DidOpenTextDocumentParams{}
   299  	opt.TextDocument.LanguageId = cli.li.lang.Reg.Language
   300  	opt.TextDocument.Version = version
   301  	opt.TextDocument.Text = text
   302  	url, err := AbsFilenameToUrl(filename)
   303  	if err != nil {
   304  		return err
   305  	}
   306  	opt.TextDocument.Uri = DocumentUri(url)
   307  	return cli.Call(ctx, "noreply:textDocument/didOpen", opt, nil)
   308  }
   309  
   310  func (cli *Client) TextDocumentDidClose(ctx context.Context, filename string) error {
   311  	// https://microsoft.github.io/language-server-protocol/specification#textDocument_didClose
   312  
   313  	opt := &DidCloseTextDocumentParams{}
   314  	url, err := AbsFilenameToUrl(filename)
   315  	if err != nil {
   316  		return err
   317  	}
   318  	opt.TextDocument.Uri = DocumentUri(url)
   319  	return cli.Call(ctx, "noreply:textDocument/didClose", opt, nil)
   320  }
   321  
   322  func (cli *Client) TextDocumentDidChange(ctx context.Context, filename, text string, version int) error {
   323  	// https://microsoft.github.io/language-server-protocol/specification#textDocument_didChange
   324  
   325  	opt := &DidChangeTextDocumentParams{}
   326  	opt.TextDocument.Version = &version
   327  	url, err := AbsFilenameToUrl(filename)
   328  	if err != nil {
   329  		return err
   330  	}
   331  	opt.TextDocument.Uri = DocumentUri(url)
   332  
   333  	// text end line/column
   334  	rd := iorw.NewStringReaderAt(text)
   335  	pos, err := OffsetToPosition(rd, len(text))
   336  	if err != nil {
   337  		return err
   338  	}
   339  
   340  	// changes
   341  	opt.ContentChanges = []*TextDocumentContentChangeEvent{
   342  		{
   343  			Range: Range{
   344  				Start: Position{0, 0},
   345  				End:   pos,
   346  			},
   347  			//RangeLength: len(text), // TODO: not working?
   348  			Text: text,
   349  		},
   350  	}
   351  	return cli.Call(ctx, "noreply:textDocument/didChange", opt, nil)
   352  }
   353  
   354  func (cli *Client) TextDocumentDidSave(ctx context.Context, filename string, text []byte) error {
   355  	// https://microsoft.github.io/language-server-protocol/specification#textDocument_didSave
   356  
   357  	opt := &DidSaveTextDocumentParams{}
   358  	opt.Text = string(text) // has omitempty
   359  	url, err := AbsFilenameToUrl(filename)
   360  	if err != nil {
   361  		return err
   362  	}
   363  	opt.TextDocument.Uri = DocumentUri(url)
   364  
   365  	return cli.Call(ctx, "noreply:textDocument/didSave", opt, nil)
   366  }
   367  
   368  //----------
   369  
   370  func (cli *Client) TextDocumentDefinition(ctx context.Context, filename string, pos Position) (*Location, error) {
   371  	// https://microsoft.github.io/language-server-protocol/specification#textDocument_definition
   372  
   373  	opt := &TextDocumentPositionParams{}
   374  	opt.Position = pos
   375  	url, err := AbsFilenameToUrl(filename)
   376  	if err != nil {
   377  		return nil, err
   378  	}
   379  	opt.TextDocument.Uri = DocumentUri(url)
   380  
   381  	result := []*Location{}
   382  	if err := cli.Call(ctx, "textDocument/definition", opt, &result); err != nil {
   383  		return nil, err
   384  	}
   385  	if len(result) == 0 {
   386  		return nil, fmt.Errorf("no results")
   387  	}
   388  	return result[0], nil // first result only
   389  }
   390  
   391  //----------
   392  
   393  func (cli *Client) TextDocumentImplementation(ctx context.Context, filename string, pos Position) (*Location, error) {
   394  	// https://microsoft.github.io/language-server-protocol/specification#textDocument_implementation
   395  
   396  	opt := &TextDocumentPositionParams{}
   397  	opt.Position = pos
   398  	url, err := AbsFilenameToUrl(filename)
   399  	if err != nil {
   400  		return nil, err
   401  	}
   402  	opt.TextDocument.Uri = DocumentUri(url)
   403  
   404  	result := []*Location{}
   405  	if err := cli.Call(ctx, "textDocument/implementation", opt, &result); err != nil {
   406  		return nil, err
   407  	}
   408  	if len(result) == 0 {
   409  		return nil, fmt.Errorf("no results")
   410  	}
   411  	return result[0], nil // first result only
   412  }
   413  
   414  //----------
   415  
   416  func (cli *Client) TextDocumentCompletion(ctx context.Context, filename string, pos Position) (*CompletionList, error) {
   417  	// https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
   418  
   419  	opt := &CompletionParams{}
   420  	opt.Context.TriggerKind = 1 // invoked
   421  	opt.Position = pos
   422  	url, err := AbsFilenameToUrl(filename)
   423  	if err != nil {
   424  		return nil, err
   425  	}
   426  	opt.TextDocument.Uri = DocumentUri(url)
   427  
   428  	result := CompletionList{}
   429  	if err := cli.Call(ctx, "textDocument/completion", opt, &result); err != nil {
   430  		return nil, err
   431  	}
   432  	//logJson(result)
   433  	return &result, nil
   434  }
   435  
   436  //----------
   437  
   438  func (cli *Client) TextDocumentDidOpenVersion(ctx context.Context, filename string, b []byte) error {
   439  
   440  	cli.lock.Lock()
   441  	v, ok := cli.lock.fversions[filename]
   442  	if !ok {
   443  		v = 1
   444  	} else {
   445  		v++
   446  	}
   447  	cli.lock.fversions[filename] = v
   448  	cli.lock.Unlock()
   449  
   450  	return cli.TextDocumentDidOpen(ctx, filename, string(b), v)
   451  }
   452  
   453  //----------
   454  
   455  //func (cli *Client) WorkspaceDidChangeWorkspaceFolders(ctx context.Context, added, removed []*WorkspaceFolder) error {
   456  //	opt := &DidChangeWorkspaceFoldersParams{}
   457  //	opt.Event = &WorkspaceFoldersChangeEvent{}
   458  //	opt.Event.Added = added
   459  //	opt.Event.Removed = removed
   460  //	err := cli.Call(ctx, "noreply:workspace/didChangeWorkspaceFolders", opt, nil)
   461  //	return err
   462  //}
   463  
   464  //----------
   465  
   466  //func (cli *Client) UpdateWorkspaceFolder(ctx context.Context, dir string) error {
   467  //	if !cli.serverCapabilities.workspace.folders {
   468  //		return nil
   469  //	}
   470  
   471  //	removed := cli.folders
   472  //	url, err := absFilenameToUrl(dir)
   473  //	if err != nil {
   474  //		return err
   475  //	}
   476  //	cli.folders = []*WorkspaceFolder{{Uri: DocumentUri(url)}}
   477  //	return cli.WorkspaceDidChangeWorkspaceFolders(ctx, cli.folders, removed)
   478  
   479  //}
   480  
   481  // TODO
   482  //return cli.WorkspaceDidChangeConfiguration(ctx, dir)
   483  //func (cli *Client) WorkspaceDidChangeConfiguration(ctx context.Context, dir string) error {
   484  //	url, err := absFilenameToUrl(dir)
   485  //	if err != nil {
   486  //		return err
   487  //	}
   488  //	//"settings":{"rootUri":"` + url + `"}
   489  //	opt := json.RawMessage(`{
   490  //		"settings":{"workspaceFolders":[{"uri":"` + url + `"}]}
   491  //	}`)
   492  //	return cli.Call(ctx, "noreply:workspace/didChangeConfiguration", opt, nil)
   493  //}
   494  
   495  //----------
   496  
   497  func (cli *Client) TextDocumentRename(ctx context.Context, filename string, pos Position, newName string) (*WorkspaceEdit, error) {
   498  	//// Commented: try it anyway
   499  	//if !cli.serverCapabilities.rename {
   500  	//	return nil, fmt.Errorf("server did not advertize rename capability")
   501  	//}
   502  
   503  	opt := &RenameParams{}
   504  	opt.NewName = newName
   505  	opt.Position = pos
   506  	url, err := AbsFilenameToUrl(filename)
   507  	if err != nil {
   508  		return nil, err
   509  	}
   510  	opt.TextDocument.Uri = DocumentUri(url)
   511  	result := WorkspaceEdit{}
   512  	err = cli.Call(ctx, "textDocument/rename", opt, &result)
   513  	return &result, err
   514  }
   515  
   516  //----------
   517  
   518  func (cli *Client) TextDocumentPrepareCallHierarchy(ctx context.Context, filename string, pos Position) ([]*CallHierarchyItem, error) {
   519  	opt := &CallHierarchyPrepareParams{}
   520  	opt.Position = pos
   521  	url, err := AbsFilenameToUrl(filename)
   522  	if err != nil {
   523  		return nil, err
   524  	}
   525  	opt.TextDocument.Uri = DocumentUri(url)
   526  	result := []*CallHierarchyItem{}
   527  	err = cli.Call(ctx, "textDocument/prepareCallHierarchy", opt, &result)
   528  	return result, err
   529  }
   530  func (cli *Client) CallHierarchyCalls(ctx context.Context, typ CallHierarchyCallType, item *CallHierarchyItem) ([]*CallHierarchyCall, error) {
   531  	method := ""
   532  	switch typ {
   533  	case IncomingChct:
   534  		method = "callHierarchy/incomingCalls"
   535  	case OutgoingChct:
   536  		method = "callHierarchy/outgoingCalls"
   537  	default:
   538  		panic("bad type")
   539  	}
   540  	opt := &CallHierarchyCallsParams{}
   541  	opt.Item = item
   542  	result := []*CallHierarchyCall{}
   543  	err := cli.Call(ctx, method, opt, &result)
   544  	return result, err
   545  }
   546  
   547  //----------
   548  
   549  func (cli *Client) TextDocumentReferences(ctx context.Context, filename string, pos Position) ([]*Location, error) {
   550  	opt := &ReferenceParams{}
   551  	opt.Context.IncludeDeclaration = true
   552  	url, err := AbsFilenameToUrl(filename)
   553  	if err != nil {
   554  		return nil, err
   555  	}
   556  	opt.TextDocument.Uri = DocumentUri(url)
   557  	opt.Position = pos
   558  	result := []*Location{}
   559  	err = cli.Call(ctx, "textDocument/references", opt, &result)
   560  	return result, err
   561  }