Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Neovim: sort imports on save #95

Closed
patrickarmengol opened this issue Apr 15, 2023 · 5 comments
Closed

Neovim: sort imports on save #95

patrickarmengol opened this issue Apr 15, 2023 · 5 comments
Labels
vim Related to the Neo(Vim) editor

Comments

@patrickarmengol
Copy link

patrickarmengol commented Apr 15, 2023

I've been trying to use ruff-lsp's built-in import sorting to get rid of isort, but I'm having trouble figuring out how to do so automatically upon saving a file. I would appreciate any help. I'm on neovim 0.9.0 and ruff-lsp 0.0.24.

background

I've already got an autocmd that calls vim.lsp.buf.format() on BufWritePre events. But it seems ruff-lsp doesn't provide formatting capability.

:lua =vim.lsp.get_active_clients()[1].server_capabilities
{
  codeActionProvider = {
    codeActionKinds = { "quickfix", "source.fixAll", "source.organizeImports", "source.fixAll.ruff", "source.organizeImports.ruff" },
    resolveProvider = true
  },
  executeCommandProvider = {
    commands = { "ruff.applyAutofix", "ruff.applyOrganizeImports" }
  },
  hoverProvider = true,
  textDocumentSync = {
    change = 2,
    openClose = true,
    save = true,
    willSave = false,
    willSaveWaitUntil = false
  },
  workspace = {
    fileOperations = vim.empty_dict(),
    workspaceFolders = {
      changeNotifications = true,
      supported = true
    }
  }
}

This seems to have been discussed recently in the following threads:
#61 - feat request to add formatting support
#64 - pull request for formatting support
#73 - reverting formatting support

my attempt

So, it looks like ruff-lsp is limiting import sorting to a code action instead. I tried setting up an autocmd to run the organize imports code action on save, similar to how I did for formatting on save with lspconfig:

      ruff_lsp = {
        on_attach = function(client, bufnr)
          -- Disable hover in favor of Pyright
          client.server_capabilities.hoverProvider = false
          -- Organize imports via code action on save
          vim.api.nvim_create_autocmd("BufWritePre", {
            callback = function()
              vim.lsp.buf.code_action { context = { only = { "source.organizeImports" } }, apply = true }
            end,
            buffer = bufnr,
          })
        end,
      },

This was modeled after the example in the gopls docs (https://github.com/golang/tools/blob/master/gopls/doc/vim.md#imports).

When I try to write the buffer with :w, the code action is executed but the file is not saved. When I run :w a second time, the file is saved, but I get a no-op notification "No code actions available".

Any ideas how to set up something like this?

@xulongwu4
Copy link

xulongwu4 commented Apr 16, 2023

The callback function for autocmd event BufWritePre should be blocking ideally so that when this function only returns after its action is completed. Neovim's vim.lsp.buf.code_action function is non-blocking, so it causes the issue you just described. My workaround for this is to add a vim.wait call inside the callback function.

The following is what worked for me:

             vim.api.nvim_create_autocmd("BufWritePre", {
               buffer = buffer,
               callback = function()
                 vim.lsp.buf.code_action({
                   context = { only = { "source.organizeImports" } },
                   apply = true,
                 })
                 vim.wait(100)
               end,
             })

@patrickarmengol
Copy link
Author

The callback function for autocmd event BufWritePre should be blocking ideally so that when this function only returns after its action is completed. Neovim's vim.lsp.buf.code_action function is non-blocking, so it causes the issue you just described. My workaround for this is to add a vim.wait call inside the callback function.

The following is what worked for me:

             vim.api.nvim_create_autocmd("BufWritePre", {
               buffer = buffer,
               callback = function()
                 vim.lsp.buf.code_action({
                   context = { only = { "source.organizeImports" } },
                   apply = true,
                 })
                 vim.wait(100)
               end,
             })

Ah, that makes sense. Strange that we cant pass in async = false like we can with vim.lsp.buf.format(). Only problem I see is that it seems this will run into issues when the action takes longer than 100ms, though I doubt that will ever happen.

I dug a little deeper and found some people utilizing vim.lsp.buf_request_sync() to await the result.

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

              local timeout = 1000 -- ms
              local result = vim.lsp.buf_request_sync(0, "textDocument/codeAction", params, timeout)
              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,
          })

But when I attempt to use this, I get the following error:

[ERROR][2023-04-16 14:43:22] .../vim/lsp/rpc.lua:734	"rpc"	"ruff-lsp"	"stderr"	"Unable to deserialize message
  + Exception Group Traceback (most recent call last):
  |   File \"/home/iu/.local/share/nvim/mason/packages/ruff-lsp/venv/lib/python3.10/site-packages/pygls/protocol.py\", line 404, in _deserialize_message
  |     return self._converter.structure(data, request_type)
  |   File \"/home/iu/.local/share/nvim/mason/packages/ruff-lsp/venv/lib/python3.10/site-packages/cattrs/converters.py\", line 309, in structure
  |     return self._structure_func.dispatch(cl)(obj, cl)
  |   File \"<cattrs generated structure lsprotocol.types.TextDocumentCodeActionRequest>\", line 26, in structure_TextDocumentCodeActionRequest
  |     if errors: raise __c_cve('While structuring ' + 'TextDocumentCodeActionRequest', errors, __cl)
  | cattrs.errors.ClassValidationError: While structuring TextDocumentCodeActionRequest (1 sub-exception)
  +-+---------------- 1 ----------------
    | Exception Group Traceback (most recent call last):
    |   File \"<cattrs generated structure lsprotocol.types.TextDocumentCodeActionRequest>\", line 10, in structure_TextDocumentCodeActionRequest
    |     res['params'] = __c_structure_params(o['params'], __c_type_params)
    |   File \"<cattrs generated structure lsprotocol.types.CodeActionParams>\", line 31, in structure_CodeActionParams
    |     if errors: raise __c_cve('While structuring ' + 'CodeActionParams', errors, __cl)
    | cattrs.errors.ClassValidationError: While structuring CodeActionParams (1 sub-exception)
    | Structuring class TextDocumentCodeActionRequest @ attribute params
    +-+---------------- 1 ----------------
      | Exception Group Traceback (most recent call last):
      |   File \"<cattrs generated structure lsprotocol.types.CodeActionParams>\", line 15, in structure_CodeActionParams
      |     res['context'] = __c_structure_context(o['context'], __c_type_context)
      |   File \"<cattrs generated structure lsprotocol.types.CodeActionContext>\", line 21, in structure_CodeActionContext
      |     if errors: raise __c_cve('While structuring ' + 'CodeActionContext', errors, __cl)
      | cattrs.errors.ClassValidationError: While structuring CodeActionContext (1 sub-exception)
      | Structuring class CodeActionParams @ attribute context
      +-+---------------- 1 ----------------
        | Traceback (most recent call last):
        |   File \"<cattrs generated structure lsprotocol.types.CodeActionContext>\", line 5, in structure_CodeActionContext
        |     res['diagnostics'] = __c_structure_diagnostics(o['diagnostics'], __c_type_diagnostics)
        | KeyError: 'diagnostics'
        | Structuring class CodeActionContext @ attribute diagnostics
        +------------------------------------

"

Additionally, each time I save a file that already has sorted imports (99% of the time), I get a "No code actions available" message. I wonder if there's a way to silence that.

@b0o
Copy link

b0o commented Apr 18, 2023

Additionally, each time I save a file that already has sorted imports (99% of the time), I get a "No code actions available" message. I wonder if there's a way to silence that.

Here's what I did:

---- rcarriga/nvim-notify
local notify = require 'notify'

notify.setup {
  render = 'default',
  stages = 'slide',
  on_open = function(win)
    vim.api.nvim_win_set_config(win, { zindex = 500 })
  end,
}

local ignored_messages = {
  'warning: multiple different client offset_encodings detected for buffer, this is not supported yet',
  'No code actions available',
}

vim.notify = function(msg, lvl, opts)
  lvl = lvl or vim.log.levels.INFO
  if vim.tbl_contains(ignored_messages, msg) then
    return
  end
  local lvls = vim.log.levels
  local keep = function()
    return true
  end
  local _opts = ({
    [lvls.TRACE] = { timeout = 500 },
    [lvls.DEBUG] = { timeout = 500 },
    [lvls.INFO] = { timeout = 1000 },
    [lvls.WARN] = { timeout = 10000 },
    [lvls.ERROR] = { timeout = 10000, keep = keep },
  })[lvl]
  opts = vim.tbl_extend('force', _opts or {}, opts or {})
  return notify.notify(msg, lvl, opts)
end

@patrickarmengol
Copy link
Author

Thanks to both of you. I will close this issue as I feel these techniques, though not the most ideal, adequately address my needs.

I also wanted to add an alternative for anyone not wanting to use codeactions. On top of getting diagnostics from ruff-lsp, you can use ruff via null-ls for formatting. This may be more relevant down the line as it seems ruff is looking to incorporate more formatting features in the future.

local null_ls = require("null-ls")

null_ls.setup({
    sources = {
        null_ls.builtins.formatting.ruff,
    }
})

@david-westreicher
Copy link

If anybody else has the problem of getting error notifications that the codeAction is not supported (in non-import-organizable files like *.md, *toml, ...) let's first check if the codeAction is supported:

vim.api.nvim_create_autocmd("BufWritePre", {
  callback = function()
    local params = vim.lsp.util.make_range_params()
    params.context = { diagnostics = vim.lsp.diagnostic.get_line_diagnostics() }

    local clients = vim.lsp.get_clients({ bufnr = 0, method = "textDocument/codeAction" })
    if #clients == 0 then
      return
    end

    local results = vim.lsp.buf_request_sync(0, "textDocument/codeAction", params)
    if not results then
      return
    end

    for _, result in pairs(results) do
      for _, action in pairs(result.result or {}) do
        if action.kind == "source.organizeImports" then
          vim.lsp.buf.code_action({ context = { only = { "source.organizeImports" } }, apply = true })
          vim.wait(100)
          break
        end
      end
    end
  end,
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
vim Related to the Neo(Vim) editor
Projects
None yet
Development

No branches or pull requests

5 participants