lsp icon indicating copy to clipboard operation
lsp copied to clipboard

How to ExecuteCommand for servers which provides custom commands

Open itsraian opened this issue 1 year ago • 10 comments

TSServer implements custom commands which can be used to trigger some actions. https://github.com/typescript-language-server/typescript-language-server?tab=readme-ov-file#workspace-commands-workspaceexecutecommand

Is there a way in the client to call those custom actions? Maybe a :LspExecuteCommand _typescript.organizeImports?

itsraian avatar Oct 09 '24 12:10 itsraian

The whole setup could be more filetype specific, see also https://github.com/yegappan/lsp/pull/562

As a new user, the list of :Lsp... commands felt long, and often redundant. Aftplugin/&ft.vim suggesting sensible default mappings could foster adaption.

Konfekt avatar Oct 10 '24 18:10 Konfekt

I suggested a new command for it, but not sure if we really need it. I mean, LSP sends the available custom commands through the executeCommandProvider capability.

We could have those commands appended to the Code Actions (since they work pretty similar)?

btw, I used tsserver as example, but figured out that ruff server used for python formatting also has some custom commands.

itsraian avatar Oct 13 '24 22:10 itsraian

I am new to this plug-in. What was missing in :help lsp-custom-commands then to implement something similar to g:LspRegisterCmdHandler('java.apply.workspaceEdit', WorkspaceEdit) for tsserver or ruff server?

doc/lsp.txt (lines 1750-1773)
14. Custom Command Handlers			*lsp-custom-commands*

When applying a code action, the language server may issue a non-standard
command.  For example, the Java language server uses non-standard commands
(e.g. java.apply.workspaceEdit).  To handle these commands, you can register a
callback function for each command using the LspRegisterCmdHandler() function.
For example: >

    vim9script
    import autoload "lsp/textedit.vim"

    def WorkspaceEdit(cmd: dict<any>)
      for editAct in cmd.arguments
	  textedit.ApplyWorkspaceEdit(editAct)
      endfor
    enddef
    g:LspRegisterCmdHandler('java.apply.workspaceEdit', WorkspaceEdit)
<
Place the above code in a file named lsp_java/plugin/lsp_java.vim and load
this plugin.

The callback function should accept a Dict argument.  The Dict argument
contains the LSP Command interface fields.  Refer to the LSP specification for
more information about the "Command" interface.

Konfekt avatar Oct 14 '24 10:10 Konfekt

I don't believe it has the behavior I need. RegisterCmdHandler listen to commands that comes from the server and do actions as needed. The example you shared (from the doc) wait for a java.apply.workspaceEdit from the server and calls textedit.Apply.. to current buffer. Its a communication Server => Client.

What I need is a way to call a command from the client. Calling a source.removeUnused.ts from the client, by example, would ask to the server remove all unused variables, which would generate a response and the client would apply as usual.

itsraian avatar Oct 14 '24 13:10 itsraian

Okay, so this is for receiving from the server instead of sending. I wonder how :help LspCodeAction solves this as it can only be applied to a diagnostic in the current line:

#  doc/lsp.txt (lines 704-718)
:LspCodeAction [query]	Apply the code action supplied by the language server
			to the diagnostic in the current line. This works only
			if there is a diagnostic message for the current line.
			You can use the ":LspDiag current" command to display
			the diagnostic for the current line.

			When [query] is given the code action starting with
			[query] will be applied. [query] can be a regexp
			pattern, or a digit corresponding to the index of the
			code actions in the created prompt.

			When [query] is not given you will be prompted to
			select one of the actions supplied by the language
			server.

But in principle

#  autoload/lsp/codeaction.vim (lines 15-26)
export def DoCommand(lspserver: dict<any>, cmd: dict<any>)
  if cmd->has_key('command') && CommandHandlers->has_key(cmd.command)
    var CmdHandler: func = CommandHandlers[cmd.command]
    try
      call CmdHandler(cmd)
    catch
      util.ErrMsg($'"{cmd.command}" handler raised exception {v:exception}')
    endtry
  else
    lspserver.executeCommand(cmd)
  endif
enddef

could be used, which at the moment is only used in CodeAction/Lens. Maybe that's what you meant with

I suggested a new command for it, but not sure if we really need it. I mean, LSP sends the available custom commands through the executeCommandProvider capability. We could have those commands appended to the Code Actions (since they work pretty similar)?

Konfekt avatar Oct 14 '24 14:10 Konfekt

You can now make custom requests to the server and map it to a command

https://github.com/yegappan/lsp/pull/640

jclsn avatar Sep 12 '25 07:09 jclsn

Thank you! Sounds like @itsraian 's concerns had been addressed?

Konfekt avatar Sep 12 '25 08:09 Konfekt

@jclsn Do I understand correctly #640 allows to send custom message asynchronously but there's no way to process response as there's currently no way to pass custom callback and pre-set one does nothing?

I am trying to run sequence of code actions to format code and organize imports as BufWritePre autocommand with gopls. I am able to use this to send message, but not able to intercept and process the response:

09/22/25 02:21:06: Sent {"method":"textDocument/codeAction","params":{"context":{"only":["source.organizeImports"]},"range":{"end":{"character":0,"line":8},"start":{"character":0,"line":0}},"textDocument":{"uri":"file:///...foo.go"}}}
09/22/25 02:21:06: Received {"id":2,"jsonrpc":"2.0","result":[{"edit":{"documentChanges":[{"edits":[{"range":{"end":{"character":11,"line":2},"start":{"character":7,"line":2}},"newText":""},{"range":{"end":{"character":0,"line":4},"start":{"character":0,"line":3}},"newText":""}],"textDocument":{"uri":"file:///...foo.go","version":3}}]},"kind":"source.organizeImports","title":"Organize Imports"}]}

As far as I understand there's no method so I can't create custom hook for the response.

blami avatar Sep 21 '25 17:09 blami

@blami I haven't looked into this. The current implementation was enough for my needs. I thought about also adding a synchronous command, but @yegappan merged it quickly without comments. It could be easily done, but no idea how to intercept the response.

There is this command, which sends an asynchronous request. You can process the reply there probably, but I haven't tried to do this.

https://github.com/yegappan/lsp/blob/21cd18535d63b18fcab58b36f07e23f161e5506b/autoload/lsp/lspserver.vim#L1050

Currently it's not doing anything with it according to this function. The responses are somehow handled and printed to the statusline though. It worked out of the box for me.

https://github.com/yegappan/lsp/blob/21cd18535d63b18fcab58b36f07e23f161e5506b/autoload/lsp/lspserver.vim#L1789

jclsn avatar Sep 21 '25 17:09 jclsn

@jclsn got it, I proposed change in #663 and also proposed to have both sync and async variant.

blami avatar Sep 21 '25 17:09 blami