Closed tkinz27 closed 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.
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).
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.
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 :)
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
?
Would you be willing to accept a PR adding a
formatting_sync
function tovim.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 ofgopls
?
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.
@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)
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
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)")
ah cool thanks for sharing @tkinz27
@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.
@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.
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.
lsp
module, the best way to get it fixed is to describe steps to reproduce it without the particular LSP server and other factors particular to your environment. Or better, by adding a failing test case to lsp_spec.lua, which has code to setup a fake LSP server to simulate various scenarios.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"
.
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
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.
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
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!
the above code also works great for me as long as I add vim.lsp.buf.formatting()
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.
Hi!
Can someone, please, summarise a full working example? Thanks in advance!
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
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.
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).
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
@alexaandru Can you add some explanations to the code? because when I hit :w, nothing happened and the result
is nil
Sorry, I can't get this to work, result
is always nil
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.
For some reason this formats the code on write but its not written .. so I have to save twice
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)
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,
})
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,
})
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
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.
Anyone aware of a way to specify the equivalent to goimports -local
?
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"
}
}
})
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 😭
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:
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 aslocal
😁
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.
autocmd BufWritePre *.go :silent! lua vim.lsp.buf.code_action({ context = { only = { "source.organizeImports" } }, apply = true })
Does any one encounter slow response time when the buffer is first time send textDocument/codeAction
to gopls?
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
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,
})
Hi!
I have read through this thread wanting to do the same thing for my setup. I ended up realizing that:
.edit
or a .command
: those can be run immediately synchronouslyvim.lsp.buf.code_action
vim.lsp.buf.code_action
is asynchronous, so it does not always resolve before Neovim has written to the filecodeAction/resolve
command, which we can call synchronously using buf_request_sync
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 🙇
I'm struggling to figure out how to make an autocmd to call the
editor.action.organizeImports
command thatgopls
offers. Forcoc.nvim
they provide the following config https://github.com/golang/tools/blob/master/gopls/doc/vim.md#cocnvimNot 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.