joaotavora / eglot

A client for Language Server Protocol servers
GNU General Public License v3.0
2.26k stars 200 forks source link

Keybindings for eglot-code-actions #411

Closed mpolden closed 3 years ago

mpolden commented 4 years ago

Is it possible to bind a key to a code action?

When writing Go it's very useful to have a keybinding for fixing imports. E.g. a keybinding for the first action offered by M-x eglot-code-actions.

nemethf commented 4 years ago

I think the default is set to the first item. So you can just press RET in completing-read.

Or are you talking about "auto-confirming a single option"?

mpolden commented 4 years ago

Yes, I suppose that could work. However, it would better if I could bind import organisation explicitly, like I can for eglot-format.

I recently migrated from lsp-mode, where organising imports is a dedication function: https://github.com/emacs-lsp/lsp-mode/blob/c7dc0ed22a3fc3c759809b7e9d8a62be2311b31c/lsp-mode.el#L4490

nemethf commented 4 years ago

elgot-format almost directly calls the LSP method textDocument/rangeFormatting. There is no such method for organizing imports.

Hm. Do you want to get code actions for the current point restricted to a given CodeActionKind? That wouldn't be hard to do.

mpolden commented 4 years ago

Do you want to get code actions for the current point restricted to a given CodeActionKind?

s/get/apply, but yes! That would be the general solution. 🙂

nemethf commented 4 years ago

Can you show me a minimal example where the "auto-confirmation of the single option" is different from the above? I.e., where there are more than one code actions for a given point and exactly one of them has source.organizeImports as its CodeActionKind.

mpolden commented 4 years ago

I've never seen any other option presented. I only use gopls though.

nemethf commented 4 years ago

clangd gives two code-actions for the following snippet at func_b.

#include <iostream>
using namespace std;

int func_a(int arg) {
  return arg + 1;
}

int main()
{
  cout << "Hello, World!";
  int a = func_b(5);
  return 0;
}

server's reply to textDocument/codeAction:

(:id 2 :result
     [(:diagnostics
       [(:code "undeclared_var_use_suggest" :message "Use of undeclared identifier 'func_b'; did you mean 'func_a'? (fix available)\n\na.cpp:8:5: note: 'func_a' declared here" :range
               (:end
                (:character 16 :line 14)
                :start
                (:character 10 :line 14))
               :severity 1 :source "clang")]
       :edit
       (:changes
        (:file:///home/nemethf/src/eglot/ccls-test/a\.cpp
         [(:newText "func_a" :range
                    (:end
                     (:character 16 :line 14)
                     :start
                     (:character 10 :line 14)))]))
       :kind "quickfix" :title "change 'func_b' to 'func_a'")
      (:command
       (:arguments
        [(:file "file:///tmp/proxy-cache/89f52e8f-d7c0-4b59-a535-175b11316b39/home/nemethf/src/eglot/ccls-test/a.cpp" :selection
                (:end
                 (:character 16 :line 14)
                 :start
                 (:character 10 :line 14))
                :tweakID "ExtractVariable")]
        :command "clangd.applyTweak" :title "Extract subexpression to variable")
       :kind "refactor" :title "Extract subexpression to variable")]
     :jsonrpc "2.0")

If we want to provide a functionality that restricts the possible code actions to a given kind ("quickfix", "refactor", or "source.organizeImports") and applies a single candidate without conformation, then it would be good to inform the user what's happening in detail. When text edits arrive in the textDocument/codeAction it's easy to provide information to the user, because the title is available ("change 'func_b' to 'func_a" in the example above). However, when the code action is a command (the second action in the example above), then the client sends an workspace/executeCommand, and the server later request a workspace/applyEdit, in which the title is not given ("Extract subexpression to variable"). I see no easy way to correlate the workspace/applyEdit with the workspace/executeCommand.

(But selecting the second choice sometimes results in a bug, so I need to investigate this further... It might be the result of my special setup, I run the servers in a docker container. )

nemethf commented 4 years ago

I haven't worked on the "auto-confirming a single option", but using the single key completion backend has similar effects. Moreover, I created a new branch wip-411-advertise-title that hopefully helps to understand the progress of automatic edits when eglot-confirm-server-initiated-edits is nil.

@mpolden, would you like to try it out and give feedback about the modified user interface?

mpolden commented 4 years ago

How is that change relevant to this issue? For my use case (organize imports), I'm never asked to confirm server edits (eglot-confirm-server-initiated-edit = 'confirm, default).

nemethf commented 4 years ago

The server communicates "import organization" through code actions. So even if this doesn't improve your use case, it definitely alters it.

jixiuf commented 3 years ago

share my implemention of eglot-organize-imports

(defun eglot-organize-imports ()
  "Offer to execute code actions `source.organizeImports'."
  (interactive)
  (unless (eglot--server-capable :codeActionProvider)
    (eglot--error "Server can't execute code actions!"))
  (let* ((server (eglot--current-server-or-lose))
         (actions (jsonrpc-request
                   server
                   :textDocument/codeAction
                   (list :textDocument (eglot--TextDocumentIdentifier))))
         (action (cl-find-if
                  (jsonrpc-lambda (&key kind &allow-other-keys)
                    (string-equal kind "source.organizeImports" ))
                  actions)))
    (when action
      (eglot--dcase action
        (((Command) command arguments)
         (eglot-execute-command server (intern command) arguments))
        (((CodeAction) edit command)
         (when edit (eglot--apply-workspace-edit edit))
         (when command
           (eglot--dbind ((Command) command arguments) command
             (eglot-execute-command server (intern command) arguments))))))))
  (add-hook 'before-save-hook #'eglot-organize-imports 30 t)
bcmills commented 3 years ago

@nemethf

If we want to provide a functionality that restricts the possible code actions to a given kind ("quickfix", "refactor", or "source.organizeImports") and applies a single candidate without conformation, then it would be good to inform the user what's happening in detail.

I think part the confusion here comes from the list of “possible code actions”.

As I see it, the use-case for this feature request is for the user to initiate a single, unambiguous, caller-specified code action (such as "source.organizeImports"), which is either currently possible or not. If the action is possible, it should be performed, and if not, then it should be skipped.

Given that, I don't think there is a strong need to “inform the user what's happening”: they know what key they pressed and they can easily find out what function that key is bound to, or else figure it out from the Emacs stack trace if need be.

Informing the user about what's going on seems nice to have, but a narrower, more targeted fix should not require that. (The wip-411-advertise-title branch seems like it's putting the cart before the horse.)

bcmills commented 3 years ago

@jixiuf, your solution doesn't work reliably for me. If try to save the file soon after loading the file and making an edit, the organizeImports action fails and Eglot emits an error:

Error: (jsonrpc-error "Edits on ‘main.go’ require version 0, you have 1" (jsonrpc-error-code . 32603) (jsonrpc-error-message . "Edits on ‘main.go’ require version 0, you have 1"))
Eglot event log ``` client-request (id:1) Mon Dec 21 21:32:30 2020: (:jsonrpc "2.0" :id 1 :method "initialize" :params (:processId 2641406 :rootPath "/tmp/tmp.80ClKpYQ1o/" :rootUri "file:///tmp/tmp.80ClKpYQ1o/" :initializationOptions #s(hash-table size 65 test eql rehash-size 1.5 rehash-threshold 0.8125 data ()) :capabilities (:workspace (:applyEdit t :executeCommand (:dynamicRegistration :json-false) :workspaceEdit (:documentChanges :json-false) :didChangeWatchedFiles (:dynamicRegistration t) :symbol (:dynamicRegistration :json-false) :configuration t) :textDocument (:synchronization (:dynamicRegistration :json-false :willSave t :willSaveWaitUntil t :didSave t) :completion (:dynamicRegistration :json-false :completionItem (:snippetSupport t) :contextSupport t) :hover (:dynamicRegistration :json-false :contentFormat ["markdown" "plaintext"]) :signatureHelp (:dynamicRegistration :json-false :signatureInformation (:parameterInformation (:labelOffsetSupport t))) :references (:dynamicRegistration :json-false) :definition (:dynamicRegistration :json-false) :declaration (:dynamicRegistration :json-false) :implementation (:dynamicRegistration :json-false) :typeDefinition (:dynamicRegistration :json-false) :documentSymbol (:dynamicRegistration :json-false :hierarchicalDocumentSymbolSupport t :symbolKind (:valueSet [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26])) :documentHighlight (:dynamicRegistration :json-false) :codeAction (:dynamicRegistration :json-false :codeActionLiteralSupport (:codeActionKind (:valueSet ["quickfix" "refactor" "refactor.extract" "refactor.inline" "refactor.rewrite" "source" "source.organizeImports"])) :isPreferredSupport t) :formatting (:dynamicRegistration :json-false) :rangeFormatting (:dynamicRegistration :json-false) :rename (:dynamicRegistration :json-false) :publishDiagnostics (:relatedInformation :json-false)) :experimental #s(hash-table size 65 test eql rehash-size 1.5 rehash-threshold 0.8125 data ())))) server-reply (id:1) Mon Dec 21 21:32:30 2020: (:jsonrpc "2.0" :result (:capabilities (:textDocumentSync (:openClose t :change 2 :save nil) :completionProvider (:triggerCharacters ["."]) :hoverProvider t :signatureHelpProvider (:triggerCharacters ["(" ","]) :definitionProvider t :typeDefinitionProvider t :implementationProvider t :referencesProvider t :documentHighlightProvider t :documentSymbolProvider t :codeActionProvider (:codeActionKinds ["quickfix" "refactor.extract" "refactor.rewrite" "source.fixAll" "source.organizeImports"]) :codeLensProvider nil :documentLinkProvider nil :workspaceSymbolProvider t :documentFormattingProvider t :documentOnTypeFormattingProvider (:firstTriggerCharacter "") :renameProvider t :foldingRangeProvider t :executeCommandProvider (:commands ["gopls.generate" "gopls.fill_struct" "gopls.regenerate_cgo" "gopls.test" "gopls.tidy" "gopls.update_go_sum" "gopls.undeclared_name" "gopls.go_get_package" "gopls.add_dependency" "gopls.upgrade_dependency" "gopls.remove_dependency" "gopls.vendor" "gopls.extract_variable" "gopls.extract_function" "gopls.gc_details" "gopls.generate_gopls_mod"]) :callHierarchyProvider t :workspace (:workspaceFolders (:supported t :changeNotifications "workspace/didChangeWorkspaceFolders"))) :serverInfo (:name "gopls" :version "{\"path\":\"golang.org/x/tools/gopls\",\"version\":\"v0.6.0-pre.1\",\"sum\":\"h1:i+eJsCS+4N+3YSxLbuq0SrfWim3V8eG4WgdK8xQdzwI=\",\"deps\":[{\"path\":\"github.com/BurntSushi/toml\",\"version\":\"v0.3.1\",\"sum\":\"h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=\"},{\"path\":\"github.com/google/go-cmp\",\"version\":\"v0.5.1\",\"sum\":\"h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=\"},{\"path\":\"github.com/sergi/go-diff\",\"version\":\"v1.1.0\",\"sum\":\"h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=\"},{\"path\":\"golang.org/x/mod\",\"version\":\"v0.3.0\",\"sum\":\"h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=\"},{\"path\":\"golang.org/x/sync\",\"version\":\"v0.0.0-20201020160332-67f06af15bc9\",\"sum\":\"h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=\"},{\"path\":\"golang.org/x/tools\",\"version\":\"v0.0.0-20201211192254-72fbef54948b\",\"sum\":\"h1:8fYBhX5ZQZtb7nVKo58TjndJwMM+cOB1xOnfjgH3uiY=\"},{\"path\":\"golang.org/x/xerrors\",\"version\":\"v0.0.0-20200804184101-5ec99f83aff1\",\"sum\":\"h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=\"},{\"path\":\"honnef.co/go/tools\",\"version\":\"v0.0.1-2020.1.6\",\"sum\":\"h1:W18jzjh8mfPez+AwGLxmOImucz/IFjpNlrKVnaj2YVc=\"},{\"path\":\"mvdan.cc/gofumpt\",\"version\":\"v0.0.0-20200927160801-5bfeb2e70dd6\",\"sum\":\"h1:z+/YqapuV7VZPvBb3GYmuEJbA88M3PFUxaHilHYVCpQ=\"},{\"path\":\"mvdan.cc/xurls/v2\",\"version\":\"v2.2.0\",\"sum\":\"h1:NSZPykBXJFCetGZykLAxaL6SIpvbVy/UFEniIfHAa8A=\"}]}")) :id 1) client-notification Mon Dec 21 21:32:30 2020: (:jsonrpc "2.0" :method "initialized" :params #s(hash-table size 65 test eql rehash-size 1.5 rehash-threshold 0.8125 data ())) client-notification Mon Dec 21 21:32:30 2020: (:jsonrpc "2.0" :method "textDocument/didOpen" :params (:textDocument (:uri "file:///tmp/tmp.80ClKpYQ1o/main.go" :version 0 :languageId "go" :text "package main\n\nfunc main() {\n type Version = module.Version\n}\n"))) client-notification Mon Dec 21 21:32:30 2020: (:jsonrpc "2.0" :method "workspace/didChangeConfiguration" :params (:settings nil)) client-request (id:2) Mon Dec 21 21:32:30 2020: (:jsonrpc "2.0" :id 2 :method "textDocument/documentSymbol" :params (:textDocument (:uri "file:///tmp/tmp.80ClKpYQ1o/main.go"))) server-notification Mon Dec 21 21:32:30 2020: (:jsonrpc "2.0" :method "window/showMessage" :params (:type 4 :message "Loading packages...")) server-request (id:1) Mon Dec 21 21:32:30 2020: (:jsonrpc "2.0" :method "workspace/configuration" :params (:items [(:scopeUri "file:///tmp/tmp.80ClKpYQ1o/" :section "gopls")]) :id 1) client-reply (id:1) Mon Dec 21 21:32:30 2020: (:jsonrpc "2.0" :id 1 :result [nil]) server-notification Mon Dec 21 21:32:30 2020: (:jsonrpc "2.0" :method "window/logMessage" :params (:type 3 :message "2020/12/21 21:32:30 go env for /tmp/tmp.80ClKpYQ1o/\n(root /tmp/tmp.80ClKpYQ1o)\n(go version go version devel +9abbe2771 Mon Dec 21 04:23:39 2020 +0000 linux/amd64)\n(valid build configuration = true)\n(build flags: [])\nGOFLAGS=-benchtime=1x\nGOMOD=/tmp/tmp.80ClKpYQ1o/go.mod\nGOPRIVATE=\nGOROOT=/usr/local/google/home/bcmills/sdk/gotip\nGOPROXY=https://proxy.golang.org,direct\nGOSUMDB=sum.golang.org\nGOMODCACHE=/tmp/tmp.80ClKpYQ1o/.gopath/pkg/mod\nGONOPROXY=\nGOCACHE=/usr/local/google/home/bcmills/.cache/go-build\nGOPATH=/tmp/tmp.80ClKpYQ1o/.gopath\nGO111MODULE=\nGOINSECURE=\nGONOSUMDB=\n\n")) server-notification Mon Dec 21 21:32:30 2020: (:jsonrpc "2.0" :method "window/logMessage" :params (:type 3 :message "2020/12/21 21:32:30 go/packages.Load\n snapshot=0\n directory=/tmp/tmp.80ClKpYQ1o\n query=[builtin example.com/...]\n packages=2\n")) server-notification Mon Dec 21 21:32:30 2020: (:jsonrpc "2.0" :method "window/showMessage" :params (:type 3 :message "Finished loading packages.")) server-request (id:2) Mon Dec 21 21:32:30 2020: (:jsonrpc "2.0" :method "client/registerCapability" :params (:registrations [(:id "workspace/didChangeWatchedFiles-0" :method "workspace/didChangeWatchedFiles" :registerOptions (:watchers [(:globPattern "**/*.{go,mod,sum}" :kind 7) (:globPattern "/tmp/tmp.80ClKpYQ1o/**/*.{go,mod,sum}" :kind 7)]))]) :id 2) client-reply (id:2) Mon Dec 21 21:32:30 2020: (:jsonrpc "2.0" :id 2 :result nil) server-notification Mon Dec 21 21:32:30 2020: (:jsonrpc "2.0" :method "textDocument/publishDiagnostics" :params (:uri "file:///tmp/tmp.80ClKpYQ1o/go.mod" :diagnostics [(:range (:start (:line 4 :character 0) :end (:line 4 :character 31)) :severity 2 :source "go mod tidy" :message "golang.org/x/mod is not used in this module")])) internal Mon Dec 21 21:32:30 2020: (:message "Diagnostics received for unvisited (file:///tmp/tmp.80ClKpYQ1o/go.mod)") server-notification Mon Dec 21 21:32:30 2020: (:jsonrpc "2.0" :method "textDocument/publishDiagnostics" :params (:uri "file:///tmp/tmp.80ClKpYQ1o/main.go" :diagnostics [(:range (:start (:line 3 :character 16) :end (:line 3 :character 22)) :severity 1 :source "compiler" :message "undeclared name: module")])) server-request (id:3) Mon Dec 21 21:32:30 2020: (:jsonrpc "2.0" :method "workspace/configuration" :params (:items [(:section "gopls")]) :id 3) client-reply (id:3) Mon Dec 21 21:32:30 2020: (:jsonrpc "2.0" :id 3 :result [nil]) server-request (id:4) Mon Dec 21 21:32:30 2020: (:jsonrpc "2.0" :method "workspace/configuration" :params (:items [(:scopeUri "file:///tmp/tmp.80ClKpYQ1o/" :section "gopls")]) :id 4) client-reply (id:4) Mon Dec 21 21:32:30 2020: (:jsonrpc "2.0" :id 4 :result [nil]) server-reply (id:2) Mon Dec 21 21:32:30 2020: (:jsonrpc "2.0" :result [(:name "main" :detail "()" :kind 12 :range (:start (:line 2 :character 0) :end (:line 4 :character 1)) :selectionRange (:start (:line 2 :character 5) :end (:line 2 :character 9)))] :id 2) client-request (id:3) Mon Dec 21 21:32:32 2020: (:jsonrpc "2.0" :id 3 :method "textDocument/codeAction" :params (:textDocument (:uri "file:///tmp/tmp.80ClKpYQ1o/main.go"))) server-reply (id:3) Mon Dec 21 21:32:32 2020: (:jsonrpc "2.0" :result [(:title "Organize Imports" :kind "source.organizeImports" :disabled (:reason "") :edit (:documentChanges [(:textDocument (:version 0 :uri "file:///tmp/tmp.80ClKpYQ1o/main.go") :edits [(:range (:start (:line 1 :character 0) :end (:line 1 :character 0)) :newText "\nimport \"golang.org/x/mod/module\"\n")])]))] :id 3) client-notification Mon Dec 21 21:32:32 2020: (:jsonrpc "2.0" :method "textDocument/didChange" :params (:textDocument (:uri "file:///tmp/tmp.80ClKpYQ1o/main.go" :version 1) :contentChanges [(:range (:start (:line 0 :character 0) :end (:line 0 :character 0)) :rangeLength 0 :text " ")])) client-notification Mon Dec 21 21:32:32 2020: (:jsonrpc "2.0" :method "textDocument/didSave" :params (:text " package main\n\nfunc main() {\n type Version = module.Version\n}\n" :textDocument (:uri "file:///tmp/tmp.80ClKpYQ1o/main.go"))) server-notification Mon Dec 21 21:33:02 2020: (:jsonrpc "2.0" :method "window/logMessage" :params (:type 3 :message "2020/12/21 21:33:02 background imports cache refresh starting\n")) server-notification Mon Dec 21 21:33:02 2020: (:jsonrpc "2.0" :method "window/logMessage" :params (:type 3 :message "2020/12/21 21:33:02 background refresh finished after 1.801979ms\n")) ```
`gopls` event log ``` [Trace - 21:32:30.340 PM] Sending request 'initialize - (1)'. Params: {"processId":2641406,"rootPath":"/tmp/tmp.80ClKpYQ1o/","rootUri":"file:///tmp/tmp.80ClKpYQ1o/","initializationOptions":{},"capabilities":{"workspace":{"applyEdit":true,"executeCommand":{"dynamicRegistration":false},"workspaceEdit":{"documentChanges":false},"didChangeWatchedFiles":{"dynamicRegistration":true},"symbol":{"dynamicRegistration":false},"configuration":true},"textDocument":{"synchronization":{"dynamicRegistration":false,"willSave":true,"willSaveWaitUntil":true,"didSave":true},"completion":{"dynamicRegistration":false,"completionItem":{"snippetSupport":true},"contextSupport":true},"hover":{"dynamicRegistration":false,"contentFormat":["markdown","plaintext"]},"signatureHelp":{"dynamicRegistration":false,"signatureInformation":{"parameterInformation":{"labelOffsetSupport":true}}},"references":{"dynamicRegistration":false},"definition":{"dynamicRegistration":false},"declaration":{"dynamicRegistration":false},"implementation":{"dynamicRegistration":false},"typeDefinition":{"dynamicRegistration":false},"documentSymbol":{"dynamicRegistration":false,"hierarchicalDocumentSymbolSupport":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]}},"documentHighlight":{"dynamicRegistration":false},"codeAction":{"dynamicRegistration":false,"codeActionLiteralSupport":{"codeActionKind":{"valueSet":["quickfix","refactor","refactor.extract","refactor.inline","refactor.rewrite","source","source.organizeImports"]}},"isPreferredSupport":true},"formatting":{"dynamicRegistration":false},"rangeFormatting":{"dynamicRegistration":false},"rename":{"dynamicRegistration":false},"publishDiagnostics":{"relatedInformation":false}},"experimental":{}}} [Trace - 21:32:30.342 PM] Received response 'initialize - (1)' in 1ms. Result: {"capabilities":{"textDocumentSync":{"openClose":true,"change":2,"save":{}},"completionProvider":{"triggerCharacters":["."]},"hoverProvider":true,"signatureHelpProvider":{"triggerCharacters":["(",","]},"definitionProvider":true,"typeDefinitionProvider":true,"implementationProvider":true,"referencesProvider":true,"documentHighlightProvider":true,"documentSymbolProvider":true,"codeActionProvider":{"codeActionKinds":["quickfix","refactor.extract","refactor.rewrite","source.fixAll","source.organizeImports"]},"codeLensProvider":{},"documentLinkProvider":{},"workspaceSymbolProvider":true,"documentFormattingProvider":true,"documentOnTypeFormattingProvider":{"firstTriggerCharacter":""},"renameProvider":true,"foldingRangeProvider":true,"executeCommandProvider":{"commands":["gopls.generate","gopls.fill_struct","gopls.regenerate_cgo","gopls.test","gopls.tidy","gopls.update_go_sum","gopls.undeclared_name","gopls.go_get_package","gopls.add_dependency","gopls.upgrade_dependency","gopls.remove_dependency","gopls.vendor","gopls.extract_variable","gopls.extract_function","gopls.gc_details","gopls.generate_gopls_mod"]},"callHierarchyProvider":true,"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":"workspace/didChangeWorkspaceFolders"}}},"serverInfo":{"name":"gopls","version":"{\"path\":\"golang.org/x/tools/gopls\",\"version\":\"v0.6.0-pre.1\",\"sum\":\"h1:i+eJsCS+4N+3YSxLbuq0SrfWim3V8eG4WgdK8xQdzwI=\",\"deps\":[{\"path\":\"github.com/BurntSushi/toml\",\"version\":\"v0.3.1\",\"sum\":\"h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=\"},{\"path\":\"github.com/google/go-cmp\",\"version\":\"v0.5.1\",\"sum\":\"h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=\"},{\"path\":\"github.com/sergi/go-diff\",\"version\":\"v1.1.0\",\"sum\":\"h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=\"},{\"path\":\"golang.org/x/mod\",\"version\":\"v0.3.0\",\"sum\":\"h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=\"},{\"path\":\"golang.org/x/sync\",\"version\":\"v0.0.0-20201020160332-67f06af15bc9\",\"sum\":\"h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=\"},{\"path\":\"golang.org/x/tools\",\"version\":\"v0.0.0-20201211192254-72fbef54948b\",\"sum\":\"h1:8fYBhX5ZQZtb7nVKo58TjndJwMM+cOB1xOnfjgH3uiY=\"},{\"path\":\"golang.org/x/xerrors\",\"version\":\"v0.0.0-20200804184101-5ec99f83aff1\",\"sum\":\"h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=\"},{\"path\":\"honnef.co/go/tools\",\"version\":\"v0.0.1-2020.1.6\",\"sum\":\"h1:W18jzjh8mfPez+AwGLxmOImucz/IFjpNlrKVnaj2YVc=\"},{\"path\":\"mvdan.cc/gofumpt\",\"version\":\"v0.0.0-20200927160801-5bfeb2e70dd6\",\"sum\":\"h1:z+/YqapuV7VZPvBb3GYmuEJbA88M3PFUxaHilHYVCpQ=\"},{\"path\":\"mvdan.cc/xurls/v2\",\"version\":\"v2.2.0\",\"sum\":\"h1:NSZPykBXJFCetGZykLAxaL6SIpvbVy/UFEniIfHAa8A=\"}]}"}} [Trace - 21:32:30.344 PM] Sending notification 'initialized'. Params: {} [Trace - 21:32:30.344 PM] Received notification 'window/showMessage'. Params: {"type":4,"message":"Loading packages..."} [Trace - 21:32:30.344 PM] Received request 'workspace/configuration - (1)'. Params: {"items":[{"scopeUri":"file:///tmp/tmp.80ClKpYQ1o/","section":"gopls"}]} [Trace - 21:32:30.352 PM] Sending notification 'textDocument/didOpen'. Params: {"textDocument":{"uri":"file:///tmp/tmp.80ClKpYQ1o/main.go","version":0,"languageId":"go","text":"package main\n\nfunc main() {\n\ttype Version = module.Version\n}\n"}} [Trace - 21:32:30.352 PM] Sending notification 'workspace/didChangeConfiguration'. Params: {"settings":null} [Trace - 21:32:30.353 PM] Sending request 'textDocument/documentSymbol - (2)'. Params: {"textDocument":{"uri":"file:///tmp/tmp.80ClKpYQ1o/main.go"}} [Trace - 21:32:30.357 PM] Sending response 'workspace/configuration - (1)' in 12ms. Result: [null] [Trace - 21:32:30.398 PM] Received notification 'window/logMessage'. Params: {"type":3,"message":"2020/12/21 21:32:30 go env for /tmp/tmp.80ClKpYQ1o/\n(root /tmp/tmp.80ClKpYQ1o)\n(go version go version devel +9abbe2771 Mon Dec 21 04:23:39 2020 +0000 linux/amd64)\n(valid build configuration = true)\n(build flags: [])\nGOFLAGS=-benchtime=1x\nGOMOD=/tmp/tmp.80ClKpYQ1o/go.mod\nGOPRIVATE=\nGOROOT=/usr/local/google/home/bcmills/sdk/gotip\nGOPROXY=https://proxy.golang.org,direct\nGOSUMDB=sum.golang.org\nGOMODCACHE=/tmp/tmp.80ClKpYQ1o/.gopath/pkg/mod\nGONOPROXY=\nGOCACHE=/usr/local/google/home/bcmills/.cache/go-build\nGOPATH=/tmp/tmp.80ClKpYQ1o/.gopath\nGO111MODULE=\nGOINSECURE=\nGONOSUMDB=\n\n"} [Trace - 21:32:30.493 PM] Received notification 'window/logMessage'. Params: {"type":3,"message":"2020/12/21 21:32:30 go/packages.Load\n\tsnapshot=0\n\tdirectory=/tmp/tmp.80ClKpYQ1o\n\tquery=[builtin example.com/...]\n\tpackages=2\n"} [Trace - 21:32:30.493 PM] Received notification 'window/showMessage'. Params: {"type":3,"message":"Finished loading packages."} [Trace - 21:32:30.493 PM] Received request 'client/registerCapability - (2)'. Params: {"registrations":[{"id":"workspace/didChangeWatchedFiles-0","method":"workspace/didChangeWatchedFiles","registerOptions":{"watchers":[{"globPattern":"**/*.{go,mod,sum}","kind":7},{"globPattern":"/tmp/tmp.80ClKpYQ1o/**/*.{go,mod,sum}","kind":7}]}}]} [Trace - 21:32:30.501 PM] Received notification 'textDocument/publishDiagnostics'. Params: {"uri":"file:///tmp/tmp.80ClKpYQ1o/go.mod","diagnostics":[{"range":{"start":{"line":4,"character":0},"end":{"line":4,"character":31}},"severity":2,"source":"go mod tidy","message":"golang.org/x/mod is not used in this module"}]} [Trace - 21:32:30.501 PM] Received notification 'textDocument/publishDiagnostics'. Params: {"uri":"file:///tmp/tmp.80ClKpYQ1o/main.go","diagnostics":[{"range":{"start":{"line":3,"character":16},"end":{"line":3,"character":22}},"severity":1,"source":"compiler","message":"undeclared name: module"}]} [Trace - 21:32:30.516 PM] Sending response 'client/registerCapability - (2)' in 22ms. Result: [Trace - 21:32:30.516 PM] Received request 'workspace/configuration - (3)'. Params: {"items":[{"section":"gopls"}]} [Trace - 21:32:30.519 PM] Sending response 'workspace/configuration - (3)' in 2ms. Result: [null] [Trace - 21:32:30.519 PM] Received request 'workspace/configuration - (4)'. Params: {"items":[{"scopeUri":"file:///tmp/tmp.80ClKpYQ1o/","section":"gopls"}]} [Trace - 21:32:30.521 PM] Sending response 'workspace/configuration - (4)' in 1ms. Result: [null] [Trace - 21:32:30.521 PM] Received response 'textDocument/documentSymbol - (2)' in 168ms. Result: [{"name":"main","detail":"()","kind":12,"range":{"start":{"line":2,"character":0},"end":{"line":4,"character":1}},"selectionRange":{"start":{"line":2,"character":5},"end":{"line":2,"character":9}}}] [Trace - 21:32:32.392 PM] Sending request 'textDocument/codeAction - (3)'. Params: {"textDocument":{"uri":"file:///tmp/tmp.80ClKpYQ1o/main.go"}} [Trace - 21:32:32.429 PM] Received response 'textDocument/codeAction - (3)' in 36ms. Result: [{"title":"Organize Imports","kind":"source.organizeImports","disabled":{"reason":""},"edit":{"documentChanges":[{"textDocument":{"version":0,"uri":"file:///tmp/tmp.80ClKpYQ1o/main.go"},"edits":[{"range":{"start":{"line":1,"character":0},"end":{"line":1,"character":0}},"newText":"\nimport \"golang.org/x/mod/module\"\n"}]}]}}] [Trace - 21:32:32.464 PM] Sending notification 'textDocument/didChange'. Params: {"textDocument":{"uri":"file:///tmp/tmp.80ClKpYQ1o/main.go","version":1},"contentChanges":[{"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":0}},"rangeLength":0,"text":" "}]} [Trace - 21:32:32.489 PM] Sending notification 'textDocument/didSave'. Params: {"text":" package main\n\nfunc main() {\n\ttype Version = module.Version\n}\n","textDocument":{"uri":"file:///tmp/tmp.80ClKpYQ1o/main.go"}} [Trace - 21:33:02.430 PM] Received notification 'window/logMessage'. Params: {"type":3,"message":"2020/12/21 21:33:02 background imports cache refresh starting\n"} [Trace - 21:33:02.431 PM] Received notification 'window/logMessage'. Params: {"type":3,"message":"2020/12/21 21:33:02 background refresh finished after 1.801979ms\n"} ```
bcmills commented 3 years ago

I have added a self-contained ELISP reproducer for the above failure mode at https://github.com/bcmills/eglot-issue-411.

To run it, clone the repo and execute emacs -q --load script.el in the repo root (with eglot and gopls installed for the current user).

mdbergmann commented 3 years ago

I would also be interested to see this eglot-organize-imports. Can this be a PR with only that?

muffinmad commented 3 years ago

The action kind can be passed as argument:

(defun eglot-code-action-kind (action-kind)
  "Offer to execute the ACTION-KIND code action."
  (interactive "sAction kind: ")
  (unless (eglot--server-capable :codeActionProvider)
    (eglot--error "Server can't execute code actions!"))
  (let* ((server (eglot--current-server-or-lose))
         (actions (jsonrpc-request
                   server
                   :textDocument/codeAction
                   (list :textDocument (eglot--TextDocumentIdentifier))))
         (action (cl-find-if
                  (jsonrpc-lambda (&key kind &allow-other-keys)
                    (string-equal kind action-kind))
                  actions)))
    (when action
      (eglot--dcase action
        (((Command) command arguments)
         (eglot-execute-command server (intern command) arguments))
        (((CodeAction) edit command)
         (when edit (eglot--apply-workspace-edit edit))
         (when command
           (eglot--dbind ((Command) command arguments) command
             (eglot-execute-command server (intern command) arguments))))))))

(defun eglot-organize-imports ()
  "Offer to execute the source.organizeImports code action."
  (interactive)
  (eglot-code-action-kind "source.organizeImports"))

(based on https://github.com/joaotavora/eglot/issues/411#issuecomment-730474005)

This will allow to create shortcuts easily.

WDYT?

mdbergmann commented 3 years ago

@muffinmad

Thanks for this snippet. I'm playing with this as well. It seems that i.e. Metals (Scala LSP server) bails out when the CodeAction request doesn't contain Range and Context parameters. But I'm unable to figure out in the spec whether those are mandatory parameters or not. In case of not it would be a problem of Metals.

mdbergmann commented 3 years ago

I had success with asking for actions like this:

(actions
          (jsonrpc-request
           server
           :textDocument/codeAction
           (list :textDocument (eglot--TextDocumentIdentifier)
                 :range (list :start (eglot--pos-to-lsp-position 0)
                              :end (eglot--pos-to-lsp-position (point-max)))
                 :context (list :diagnostics []
                                :only ["source.organizeImports"]))))
muffinmad commented 3 years ago

Looks almost like https://github.com/joaotavora/eglot/blob/0c4daa40c53ede22f964d3f9ac1c20752bf97141/eglot.el#L2538-L2546

Maybe the better way would be to extract the actions retrieval code from the eglot-code-actions function.

muffinmad commented 3 years ago

Or make the eglot-code-actions function to accept the action kind to run.

mdbergmann commented 3 years ago

Yes, it's similar. Though with fixed start and end positions spawning the whole document and for the server the instruction to only include the requested CodeAction, if available. Not sure though if this warrants such an amount of code duplication.

muffinmad commented 3 years ago

@joaotavora Do you think those beg and end arguments are commonly used? (https://github.com/joaotavora/eglot/commit/d6af4df11ad962e38d0f8ab9306e7c664ff89ac5)

IMO replacing them with action-kind can make more benefit: code action of the specific kind can be applied without the need to select the action from the list.

mdbergmann commented 3 years ago

The organize Imports is working for me on Metals only like this:

(defun eglot-organize-imports ()
  "Offer to execute code actions `source.organizeImports'."
  (interactive)
  (unless (eglot--server-capable :codeActionProvider)
    (eglot--error "Server can't execute code actions!"))
  (let* ((server (eglot--current-server-or-lose))
         (actions
          (jsonrpc-request
           server
           :textDocument/codeAction
           (list :textDocument (eglot--TextDocumentIdentifier)
                 :range (list :start (eglot--pos-to-lsp-position 0)
                              :end (eglot--pos-to-lsp-position (point-max)))
                 :context (list :diagnostics []
                                :only ["source.organizeImports"]))))
         (action (car (mapcar (jsonrpc-lambda (&rest all &allow-other-keys)
                                all)
                              actions))))
    (message "Action: %s" action)
    (when action
      (eglot--dcase action
        (((CodeAction) edit command)
         (when edit (eglot--apply-workspace-edit edit))
         (when command
           (eglot--dbind ((Command) command arguments) command
                         (eglot-execute-command server (intern command) arguments))))))))

Not sure if this jsonrpc-lambda can be done any simpler. But, a CodeAction request must have Range and Context parameters. In the spec only the parameters with ? are optional.

I think for general code-actions the begin and end range is useful and should be kept.

joaotavora commented 3 years ago

@muffinmad , I didn't follow this very closely, but I think you're on to something by adding optional arguments to eglot-code-actions.

@joaotavora Do you think those beg and end arguments are commonly used? (d6af4df)

As you can read the current code, an interactive call sets at least beg, meaning "I want code actions right here". And if there's a region both are used. So I wouldn't get rid of them.

But your filtering can be implemented as an extra argument, why not? If the dumb optionality of Elisp defuns isn't pretty, feel free to use a cl-defun and &key arguments. Just remember to make the interactive spec do the right thing. This isn't 100% backward compatible, but shouldn't be terribly problematic.

IMO replacing them with action-kind can make more benefit: code action of the specific kind can be applied without the need to select the action from the list.

In my opinion, replacing is bad, but adding this option is very good. You can even make C-u M-x eglot-code-actions prompt the user for a kind of code actions to select automatically (maybe even with completion?).

Thanks everybody, I like where this is going.

mdbergmann commented 3 years ago

In my opinion, replacing is bad, but adding this option is very good. You can even make C-u M-x eglot-code-actions prompt the user for a kind of code actions to select automatically (maybe even with completion?).

That sounds like a good idea.

muffinmad commented 3 years ago

@joaotavora Do you think those beg and end arguments are commonly used? (d6af4df)

As you can read the current code, an interactive call sets at least beg, meaning "I want code actions right here". And if there's a region both are used. So I wouldn't get rid of them.

I'm not proposing to get rid of the beg and end variables, but only of the beg and end arguments. I mean, does someone uses it like (eglot-code-actions 1 1)? Eglot itself doesn't use it like that, but move point before calling eglot-code-actions:

https://github.com/joaotavora/eglot/blob/0c4daa40c53ede22f964d3f9ac1c20752bf97141/eglot.el#L1469-L1471

If the dumb optionality of Elisp defuns isn't pretty,

Adding another optional argument will look like this:

(defun eglot-code-actions (beg &optional end action-kind)

So there are no simply way to just call (eglot-code-actions "some.action.kind").

feel free to use a cl-defun and &key arguments.

I'll take a look at cl-defun, thanks!

In my opinion, replacing is bad, but adding this option is very good. You can even make C-u M-x eglot-code-actions prompt the user for a kind of code actions to select automatically (maybe even with completion?).

Ok, let's try this approach!

joaotavora commented 3 years ago

I mean, does someone uses it like (eglot-code-actions 1 1)? Eglot itself doesn't use it like that, but move point before calling eglot-code-actions:

Eglot doesn't even call it, as far as I can read in the code. It is currently meant primarily for interactive use.

So there are no simply way to just call (eglot-code-actions "some.action.kind").

But are you saying that adding (point-min) and (point-max) is too much to type for users putting this in some hook their .emacs? Meh, I don't know. But that's why I propose :kind "some.action.kind" (i.e. the cl-defun) and we can default the other args to something logical. Of course, don't forget that it still must work interactively.

mdbergmann commented 3 years ago

It is currently meant primarily for interactive use.

Yes. It's called from mouse clicks, with the proper range (beg, end) of the flymake diagnostic in question. And that works. So this should continue to work.

A third parameter might be OK, when optional. Obviously it should stay unset when called from mouse click.