neovim / nvim-lspconfig

Quickstart configs for Nvim LSP
Apache License 2.0
10.71k stars 2.08k forks source link

[gopls] run `editor.action.organizeImports` on save. #115

Closed tkinz27 closed 4 years ago

tkinz27 commented 4 years ago

I'm struggling to figure out how to make an autocmd to call the editor.action.organizeImports command that gopls offers. For coc.nvim they provide the following config https://github.com/golang/tools/blob/master/gopls/doc/vim.md#cocnvim

Not sure if this is the write project to add this type of support or if it is just a lua function that calls the built in vim.lsp functions. Any help would be appreciated though.

lithammer commented 4 years ago

Just winging it here, but something like this could be a starting point:

autocmd BufWritePost * lua vim.lsp.buf.formatting()

It's pretty naive. So using the on_attach callback is probably a better approach:

local nvim_lsp = require("nvim_lsp")

local on_attach = function(_, bufnr)
  vim.api.nvim_command("au BufWritePost <buffer> lua vim.lsp.buf.formatting()")
end

nvim_lsp.gopls.setup({ on_attach = on_attach })

It works, kind of, but leaves the file a 'modified' state which isn't desirable. So there's at least one missing piece.

tkinz27 commented 4 years ago

So that autocmd works alright, however gopls separates out updating imports as a separate action than formatting. I guess my question is how to call a custom lsp action (not sure the lsp terms here).

gbrlsnchs commented 4 years ago

So that autocmd works alright, however gopls separates out updating imports as a separate action than formatting. I guess my question is how to call a custom lsp action (not sure the lsp terms here).

What you want to run is a code action. "Organize imports" is one of the gopls code actions.

dlukes commented 4 years ago

It works, kind of, but leaves the file a 'modified' state which isn't desirable. So there's at least one missing piece.

The formatting params in https://github.com/neovim/neovim/blob/9678fe4cfba9f7a9dacbd6d5a56c58241e98aa60/runtime/lua/vim/lsp/buf.lua#L73-L85 could be extracted into a helper function, and there could be an additional formatting_sync function. Something along the following lines:

local function formatting_params(options)
  validate { options = {options, 't', true} }
  local sts = vim.bo.softtabstop;
  options = vim.tbl_extend('keep', options or {}, {
    tabSize = (sts > 0 and sts) or (sts < 0 and vim.bo.shiftwidth) or vim.bo.tabstop;
    insertSpaces = vim.bo.expandtab;
  })
  return {
    textDocument = { uri = vim.uri_from_bufnr(0) };
    options = options;
  }
end

function M.formatting(options)
  return request('textDocument/formatting', params)
end

function M.formatting_sync(options, timeout)
  local params = formatting_params(options)
  local result = vim.lsp.buf_request_sync(0, "textDocument/formatting", params, timeout)
  if not result then return end
  result = result[1].result
  vim.lsp.util.apply_text_edits(result)
end

This would allow formatting to be run synchronously on BufWritePre (possibly increasing the default 100ms timeout to e.g. 1000ms), so that the file isn't left in a modified state:

vim.api.nvim_command("au BufWritePre <buffer> lua vim.lsp.buf.formatting_sync(nil, 1000)")

I know OP actually wants something slightly different, but since I got here looking for the above and got halfway there thanks to @lithammer's snippet, I figured I'd post the rest since it might help other people too :)

dlukes commented 4 years ago

Would you be willing to accept a PR adding a formatting_sync function to vim.lsp.buf? Or do you feel that a more general solution is needed, since there are other actions which might need to be run in a synchronous fashion (e.g. on save), like that "organize imports" code action of gopls?

gbrlsnchs commented 4 years ago

Would you be willing to accept a PR adding a formatting_sync function to vim.lsp.buf? Or do you feel that a more general solution is needed, since there are other actions which might need to be run in a synchronous fashion (e.g. on save), like that "organize imports" code action of gopls?

I think there's a reason why a formatting_sync equivalent exists in most LSP clients: many people use it. I'm not a Neovim maintainer, but IMO, it would be a nice addition to the official API. Otherwise people will either have to implement the function themselves or use a third-party plugin.

tjdevries commented 4 years ago

@dlukes I think that would make a good PR where we can talk about where that should live and how we can add improvements like this. If you make the PR and place it somewhere sensible to start, we can chat about it in that PR (which would be a better place).

(As a note, the PR would be on neovim, not nvim-lsp. Just to be clear)

sentriz commented 4 years ago

as of neovim/neovim#11607

autocmd BufWritePre *.go lua vim.lsp.buf.code_action({ source = { organizeImports = true } })

this seems to kinda work, except it always prompts you what to do

tkinz27 commented 4 years ago

So i'm still testing right now but this is what I'm trying out. I dont think its working quite right though but it at least doesn't prompt me.

-- organize imports sync
function go_org_imports(options, timeout_ms)
  local context = { source = { organizeImports = true } }
  vim.validate { context = { context, 't', true } }
  local params = vim.lsp.util.make_range_params()
  params.context = context

  local results = vim.lsp.buf_request_sync(0, "textDocument/codeAction", params, timeout_ms)
  print(vim.inspect(result))
  if not result then return end
  vim.lsp.util.apply_text_edits(result[1].result)

  -- local params = vim.lsp.util.make_formatting_params(options)
  -- local result = vim.lsp.buf_request_sync(0, "textDocument/formatting", params, timeout_ms)
  -- if not result then return end
  -- result = result[1].result
  -- vim.lsp.util.apply_text_edits(result)
end

vim.api.nvim_command("au BufWritePre *.go lua go_org_imports({}, 1000)")
sentriz commented 4 years ago

ah cool thanks for sharing @tkinz27

aktau commented 4 years ago

@tkinz27 your example doesn't work because the return type of textDocument/codeAction is different from textDocument/formatting. Only after figuring this out by trial and error did I realize I should've looked at nvim-lsp's own textDocument/codeAction implementation.

The modified (verified working as of right now) version is:

-- Synchronously organise (Go) imports.
function go_organize_imports_sync(timeout_ms)
  local context = { source = { organizeImports = true } }
  vim.validate { context = { context, 't', true } }
  local params = vim.lsp.util.make_range_params()
  params.context = context

  local result = vim.lsp.buf_request_sync(0, "textDocument/codeAction", params, timeout_ms)
  if not result then return end
  result = result[1].result
  if not result then return end
  edit = result[1].edit
  vim.lsp.util.apply_workspace_edit(edit)
end

Though it's not perfect, because we should be emulating the codeAction handler to be resistant to changes in gopls implementation.

tkinz27 commented 4 years ago

@aktau thanks for the update, yeah I hadn't really verified that mine was working. Been stuck workign on terraform stuff and haven't worked in go for a little while. Thanks for sharing.

justinmk commented 4 years ago

Thanks for your report. The configs here are best-effort:

It is hoped that these configurations serve as a "source of truth", but they are strictly best effort. If something doesn't work, these configs are useful as a starting point, which you can adjust to fit your environment.

dialtone commented 3 years ago

Sadly using latest neovim HEAD with gopls 0.6.1 and latest version of nvim-lspconfig it seems that while this code works, it doesn't really work when you are trying to import non standard library modules like dependencies in go.mod and vendored.

Executing a print(vim.inspect(result)) of the call returns { {} } if I remove a module, say "go.uber.org/zap", while it returns a nicer full table if I remove "time".

desdic commented 3 years ago

I had a problem when using 'gd' goto definitions my go imports gave an index error. Turns out that the above script uses index[1] but that is only when a file is open directly. If a function opens the file it gets index 2. So my modified versions looks like this:

function go_organize_imports_sync(timeoutms)
    local context = {source = {organizeImports = true}}
    vim.validate {context = {context, 't', true}}

    local params = vim.lsp.util.make_range_params()
    params.context = context

    local method = 'textDocument/codeAction'
    local resp = vim.lsp.buf_request_sync(0, method, params, timeoutms)

    -- imports is indexed with clientid so we cannot rely on index always is 1
    for _, v in next, resp, nil do
      local result = v.result
      if result and result[1] then
        local edit = result[1].edit
        vim.lsp.util.apply_workspace_edit(edit)
      end
    end
    -- Always do formating
    vim.lsp.buf.formatting()
end
ldelossa commented 3 years ago

Hey all,

Got to here from: https://github.com/golang/tools/blob/master/gopls/doc/vim.md#neovim-imports

I'm not sure if you are all aware, but when you're utilizing this function on save, it appears any code action that's available to you on the given line will be called.

This is an issue when you're saving to add an import, but your cursor is in an incomplete struct, the code action to fill in all the struct fields are ran unconditionally, while your intent was only to import a missing package.

desdic commented 3 years ago

Hey all,

Got to here from: https://github.com/golang/tools/blob/master/gopls/doc/vim.md#neovim-imports

I'm not sure if you are all aware, but when you're utilizing this function on save, it appears any code action that's available to you on the given line will be called.

This is an issue when you're saving to add an import, but your cursor is in an incomplete struct, the code action to fill in all the struct fields are ran unconditionally, while your intent was only to import a missing package.

Haven't had that problem but I guess it should be pretty easy to modify the function to test which action it is and only take action if its the wanted code action

alexaandru commented 3 years ago

This is correct, I can reproduce it. If you look at it, at the beginning it sets the context (that will be passed with the code action request) to local context = {source = {organizeImports = true}} - I modified that to some bogus value (for completeness, to organizeImportsz) and the imports were STILL organized. Which means the context was not used at all.

I then went back to the spec, and apparently, the context should look like this instead: https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#codeActionContext That's quite different from what is in the solution given in here. So I used that and it seems to actually work as expected. If I give the correct value it works, if I give a bogus value it doesn't. So in theory, it should filter out unwanted actions. This is the full method that I use, FWIW:

    function util.OrgImports(wait_ms)
      local params = vim.lsp.util.make_range_params()
      params.context = {only = {"source.organizeImports"}}
      local result = vim.lsp.buf_request_sync(0, "textDocument/codeAction", params, wait_ms)
      for _, res in pairs(result or {}) do
        for _, r in pairs(res.result or {}) do
          if r.edit then
            vim.lsp.util.apply_workspace_edit(r.edit)
          else
            vim.lsp.buf.execute_command(r.command)
          end
        end
      end
    end

Oh, I tested this on Go files (so gopls).

Hope it helps!

desdic commented 3 years ago

the above code also works great for me as long as I add vim.lsp.buf.formatting()

dialtone commented 3 years ago

The code above seems to work although now it seems the imports are re-organized but code is formatted again after the save has happened so it needs a new command to again save the buffer.

jurisbu commented 3 years ago

Hi!

Can someone, please, summarise a full working example? Thanks in advance!

zapling commented 3 years ago

Not sure if it's the best practice, but this seems to be working fine for me. I have put this in a ~/.config/nvim/ftplugin/go/lsp.vim

lua <<EOF
    function org_imports(wait_ms)
      local params = vim.lsp.util.make_range_params()
      params.context = {only = {"source.organizeImports"}}
      local result = vim.lsp.buf_request_sync(0, "textDocument/codeAction", params, wait_ms)
      for _, res in pairs(result or {}) do
        for _, r in pairs(res.result or {}) do
          if r.edit then
            vim.lsp.util.apply_workspace_edit(r.edit)
          else
            vim.lsp.buf.execute_command(r.command)
          end
        end
      end
  end
EOF

augroup GO_LSP
    autocmd!
    autocmd BufWritePre *.go :silent! lua vim.lsp.buf.formatting()
    autocmd BufWritePre *.go :silent! lua org_imports(3000)
augroup END
rverton commented 2 years ago

Just a little heads up for everyone using this workaround from @zapling with a recent neovim version. With the current master (f4300985d3212887ef27d703ba8cb4230813e095), this does not work anymore and imports are not automatically added.

alexaandru commented 2 years ago

That is due to https://github.com/neovim/neovim/issues/14090#issuecomment-1012005684 , the offset_encoding is now a required parameter for vim.lsp.util.apply_workspace_edit() & friends.

Changing vim.lsp.util.apply_workspace_edit(r.edit) to vim.lsp.util.apply_workspace_edit(r.edit, "utf-16") should do for most LSPs (including gopls) or you can follow the instructions in the issue above mentioned for a more correct solution (i.e. detect the offset encoding and use that).

zapling commented 2 years ago

Just a little heads up for everyone using this workaround from @zapling with a recent neovim version. With the current master (f4300985d3212887ef27d703ba8cb4230813e095), this does not work anymore and imports are not automatically added.

Thanks for the heads up. The fix should is probably be like this, as @alexaandru mentioned.

          if r.edit then
-            vim.lsp.util.apply_workspace_edit(r.edit)
+            vim.lsp.util.apply_workspace_edit(r.edit, "utf-16")
          else
juniway commented 2 years ago

@alexaandru Can you add some explanations to the code? because when I hit :w, nothing happened and the result is nil

juniway commented 2 years ago

Sorry, I can't get this to work, result is always nil

jan-xyz commented 2 years ago

you can just dump this anywhere in your Neovim lua config:

organize imports aka goimports:

vim.api.nvim_create_autocmd("BufWritePre", {
    pattern = { "*.go" },
    callback = function()
        local params = vim.lsp.util.make_range_params(nil, "utf-16")
        params.context = { only = { "source.organizeImports" } }
        local result = vim.lsp.buf_request_sync(0, "textDocument/codeAction", params, 3000)
        for _, res in pairs(result or {}) do
            for _, r in pairs(res.result or {}) do
                if r.edit then
                    vim.lsp.util.apply_workspace_edit(r.edit, "utf-16")
                else
                    vim.lsp.buf.execute_command(r.command)
                end
            end
        end
    end,
})

code formatting aka gofmt:

vim.api.nvim_create_autocmd("BufWritePre", {
    pattern = { "*.go" },
    callback = function()
        vim.lsp.buf.formatting_sync(nil, 500)
    end,
})

(mostly taken from https://github.com/neovim/nvim-lspconfig/issues/115#issuecomment-902680058

EDIT: Updated the gofmt suggestion to do format_sync and have a longer timeout. The previous suggestion set vim.lsp.buf.formatting as the callback.

desdic commented 2 years ago

For some reason this formats the code on write but its not written .. so I have to save twice

rafaelsq commented 2 years ago

For some reason this formats the code on write but its not written .. so I have to save twice

try to increment the timeout (from 3000 to something like 5000 or more)

jan-xyz commented 2 years ago

For some reason this formats the code on write but its not written .. so I have to save twice

You can try:

vim.api.nvim_create_autocmd("BufWritePre", {
    pattern = { "*.go" },
    callback = function()
        vim.lsp.buf.formatting_sync(nil, 500)
    end,
})
rafaelsq commented 2 years ago

formatting_sync was deprecated. You should use vim.lsp.buf.format

And when I try to use 3000 for the timeout, I have to save twice as welll. What I did to fix it was to change to 5000(5s)

function org_imports()
  local clients = vim.lsp.buf_get_clients()
  for _, client in pairs(clients) do

    local params = vim.lsp.util.make_range_params(nil, client.offset_encoding)
    params.context = {only = {"source.organizeImports"}}

    local result = vim.lsp.buf_request_sync(0, "textDocument/codeAction", params, 5000)
    for _, res in pairs(result or {}) do
      for _, r in pairs(res.result or {}) do
        if r.edit then
          vim.lsp.util.apply_workspace_edit(r.edit, client.offset_encoding)
        else
          vim.lsp.buf.execute_command(r.command)
        end
      end
    end
  end
end

vim.api.nvim_create_autocmd("BufWritePre", {
  pattern = { "*.go" },
  callback = vim.lsp.buf.format,
})

vim.api.nvim_create_autocmd("BufWritePre", {
  pattern = { "*.go" },
  callback = org_imports,
})
desdic commented 2 years ago

For some reason this formats the code on write but its not written .. so I have to save twice

try to increment the timeout (from 3000 to something like 5000 or more)

I just find it soo nasty. Its like having sleep in my code and I'm not a fan .. wish it would fail and give an error if it got a timeout

sudomateo commented 2 years ago

It's true that vim.lsp.buf.formatting_sync was deprecated (more information on the deprecation in https://github.com/neovim/neovim/issues/18371). However, unless you're building from the default branch you won't have access to vim.lsp.buf.format yet. I'm running Neovim v0.7.0 with the following configuration for Go:

vim.api.nvim_create_autocmd("BufWritePre", {
  pattern = { "*.go" },
  callback = function()
      vim.lsp.buf.formatting_sync(nil, 3000)
  end,
})

vim.api.nvim_create_autocmd("BufWritePre", {
    pattern = { "*.go" },
    callback = function()
        local params = vim.lsp.util.make_range_params(nil, vim.lsp.util._get_offset_encoding())
        params.context = {only = {"source.organizeImports"}}

        local result = vim.lsp.buf_request_sync(0, "textDocument/codeAction", params, 3000)
        for _, res in pairs(result or {}) do
            for _, r in pairs(res.result or {}) do
                if r.edit then
                    vim.lsp.util.apply_workspace_edit(r.edit, vim.lsp.util._get_offset_encoding())
                else
                    vim.lsp.buf.execute_command(r.command)
                end
            end
        end
    end,
})

I'm using vim.lsp.util._get_offset_encoding() to get the offset encoding currently. I'm not sure if there's a better way. If anyone has any suggestions, please let me know.

chancez commented 2 years ago

Anyone aware of a way to specify the equivalent to goimports -local?

sentriz commented 2 years ago

Anyone aware of a way to specify the equivalent to goimports -local?

when in your gopls settings there is a "local" setting https://github.com/golang/tools/blob/master/gopls/doc/settings.md#local-string

i think something like

lspconfig.gopls.setup({
    settings = {
        gopls = {
            local = "example.com"
        }
    }
})
chancez commented 2 years ago

Ah yes, I needed to look more carefully. Though I hadn't realized it was a prefix, that's not very useful as-is. I'll have to figure out a way to automagically get that value 😭

sentriz commented 2 years ago

Ah yes, I needed to look more carefully. Though I hadn't realized it was a prefix, that's not very useful as-is. I'll have to figure out a way to automagically get that value sob

i wonder if you could do something funny like have lua execute the equivalent of go mod edit -json | jq .Module.Path and use that as local :grin:

chancez commented 2 years ago

Ah yes, I needed to look more carefully. Though I hadn't realized it was a prefix, that's not very useful as-is. I'll have to figure out a way to automagically get that value sob

i wonder if you could do something funny like have lua execute the equivalent of go mod edit -json | jq .Module.Path and use that as local 😁

I wrote a simple helper function in Lua:

-- see if the file exists
function FileExists(file)
  local f = io.open(file, "rb")
  if f then f:close() end
  return f ~= nil
end

-- Get the value of the module name from go.mod in PWD
function GetGoModuleName()
  if not FileExists("go.mod") then return nil end
  for line in io.lines("go.mod") do
    if vim.startswith(line, "module") then
      local items = vim.split(line, " ")
      local module_name = vim.trim(items[2])
      return module_name
    end
  end
  return nil
end

local goModule = GetGoModuleName()
...

local servers = {
  goals = {
    ["local"] = goModule,
  }
}

That said, it seems like organizing imports doesn't always behave as I would expect when the local setting is configured, but I'm not sure why.

yaocccc commented 2 years ago

autocmd BufWritePre *.go :silent! lua vim.lsp.buf.code_action({ context = { only = { "source.organizeImports" } }, apply = true })

leonasdev commented 1 year ago

Does any one encounter slow response time when the buffer is first time send textDocument/codeAction to gopls?

leonasdev commented 1 year ago

Does any one encounter slow response time when the buffer is first time send textDocument/codeAction to gopls?

I wrote some hacks to prevent blocking when save buffer on first time opened (send a preflight request to gopls when lsp attached):

local golang_organize_imports = function(bufnr, isPreflight)
  local params = vim.lsp.util.make_range_params(nil, vim.lsp.util._get_offset_encoding(bufnr))
  params.context = { only = { "source.organizeImports" } }

  if isPreflight then
    vim.lsp.buf_request(bufnr, "textDocument/codeAction", params, function() end)
    return
  end

  local result = vim.lsp.buf_request_sync(bufnr, "textDocument/codeAction", params, 3000)
  for _, res in pairs(result or {}) do
    for _, r in pairs(res.result or {}) do
      if r.edit then
        vim.lsp.util.apply_workspace_edit(r.edit, vim.lsp.util._get_offset_encoding(bufnr))
      else
        vim.lsp.buf.execute_command(r.command)
      end
    end
  end
end

vim.api.nvim_create_autocmd("LspAttach", {
  group = vim.api.nvim_create_augroup("LspFormatting", {}),
  callback = function(args)
    local bufnr = args.buf
    local client = vim.lsp.get_client_by_id(args.data.client_id)

    if client.name == "gopls" then
      -- hack: Preflight async request to gopls, which can prevent blocking when save buffer on first time opened
      golang_organize_imports(bufnr, true)

      vim.api.nvim_create_autocmd("BufWritePre", {
        pattern = "*.go",
        group = vim.api.nvim_create_augroup("LspGolangOrganizeImports." .. bufnr, {}),
        callback = function()
          golang_organize_imports(bufnr)
        end,
      })
    end
  end,
})

comparision demo:

https://github.com/neovim/nvim-lspconfig/assets/39915562/18b10465-8765-4964-ae60-87f1f48c4167

yoshiya0503 commented 1 month ago

I'm simply doing like this.

vim.api.nvim_create_autocmd("BufWritePre", {
  callback = function(args)
    vim.lsp.buf.format()
    vim.lsp.buf.code_action { context = { only = { 'source.organizeImports' } }, apply = true }
    vim.lsp.buf.code_action { context = { only = { 'source.fixAll' } }, apply = true }
  end,
})
fnune commented 2 weeks ago

Hi!

I have read through this thread wanting to do the same thing for my setup. I ended up realizing that:

I've put this all together into a snippet here: https://github.com/fnune/codeactions-on-save.nvim/blob/06ee93541ae32d335e5614ea389b2703a31cb658/lua/codeactions-on-save/main.lua#L15-L70

You can use it as a plugin if you'd like: https://github.com/fnune/codeactions-on-save.nvim

Thanks for this thread 🙇