astral-sh/ruff-lsp

Enable import autosort on save

steakhutzeee opened this issue · 14 comments

Hi,

i'm trying to configure ruff in Neovim. I'm using this config:

return {
  -- https://github.com/VonHeikemen/lsp-zero.nvim
  
  'VonHeikemen/lsp-zero.nvim',
  branch = 'v3.x',
  dependencies = {
    {'williamboman/mason.nvim'},
    {'williamboman/mason-lspconfig.nvim'},
    {'neovim/nvim-lspconfig'},
    {'hrsh7th/cmp-nvim-lsp'},
    {'hrsh7th/nvim-cmp'},
    {'L3MON4D3/LuaSnip'},
  },
  config = function()
    local lsp_zero = require('lsp-zero')
    local navic = require('nvim-navic')

    lsp_zero.on_attach(function(client, bufnr)
      -- see :help lsp-zero-keybindings
      -- to learn the available actions
      lsp_zero.default_keymaps({
        buffer = bufnr,
        preserve_mappings = false
      })
      
      -- For nvim-ufo
      lsp_zero.set_server_config({
        capabilities = {
          textDocument = {
            foldingRange = {
              dynamicRegistration = false,
              lineFoldingOnly = true
            }
          }
        }
      })
      
      -- Enable navic
      if client.server_capabilities.documentSymbolProvider then
        navic.attach(client, bufnr)
      end
      
      -- Disable hover in favor of Pyright
      if client.name == 'ruff_lsp' then
        -- Disable hover in favor of Pyright
        client.server_capabilities.hoverProvider = false
      end
    end)
    
    -- Ruff command to organize imports on save
    ---@param name string
    ---@return lsp.Client
    local function lsp_client(name)
      return assert(
        vim.lsp.get_active_clients({ bufnr = vim.api.nvim_get_current_buf(), name = name })[1],
        ('No %s client found for the current buffer'):format(name)
      )
    end
    
    local servers = {
      ruff_lsp = {
        init_options = {
          settings = {
            -- ...
          },
        },
        commands = {
          RuffAutofix = {
            function()
              lsp_client('ruff_lsp').request("workspace/executeCommand", {
                command = 'ruff.applyAutofix',
                arguments = {
                  { uri = vim.uri_from_bufnr(0) },
                },
              })
            end,
            description = 'Ruff: Fix all auto-fixable problems',
          },
          RuffOrganizeImports = {
            function()
              lsp_client('ruff_lsp').request("workspace/executeCommand", {
                command = 'ruff.applyOrganizeImports',
                arguments = {
                  { uri = vim.uri_from_bufnr(0) },
                },
              })
            end,
            description = 'Ruff: Format imports',
          },
        },
      },
    }
    
    -- don't add this function in the `on_attach` callback.
    -- `format_on_save` should run only once, before the language servers are active.
    lsp_zero.format_on_save({
      format_opts = {
        async = false,
        timeout_ms = 10000,
      },
      servers = {
        ['ruff_lsp'] = {'python'},
      }
    })
    
    lsp_zero.set_sign_icons({
      error = '✘',
      warn = '▲',
      hint = '⚑',
      info = '»'
    })
    
    -- cmp configuration
    local cmp = require('cmp')
    
    cmp.setup({
      -- Preselect first item
      preselect = 'item',
      completion = {
        completeopt = 'menu,menuone,noinsert'
      },
      window = {
        completion = cmp.config.window.bordered(),
        documentation = cmp.config.window.bordered(),
      }
    
    })
    
    -- to learn how to use mason.nvim with lsp-zero
    -- read this: https://github.com/VonHeikemen/lsp-zero.nvim/blob/v3.x/doc/md/guide/integrate-with-mason-nvim.md
    require('mason').setup({})
    require('mason-lspconfig').setup({
      ensure_installed = {'ruff_lsp', 'pyright'},
      handlers = {
        lsp_zero.default_setup,
        ruff_lsp = function()
          require('lspconfig').ruff_lsp.setup({
            on_attach = on_attach,
            init_options = {
              settings = {
                -- CLI arguments
                args = {
                 "pyproject.toml"
                },
              },
            },
          })
        end,
        pyright = function()
          require('lspconfig').pyright.setup {
            on_attach = on_attach,
            settings = {
              pyright = {
                -- Using Ruff's import organizer
                disableOrganizeImports = true,
              },
              python = {
                analysis = {
                  -- Ignore all files for analysis to exclusively use Ruff for linting
                  ignore = { '*' },
                },
              },
            },
          }
        end,      
      },
    })
  end,
}

so i'm formatting the code on save, but how to also sort the imports on save? I looked at other issues on Github but it's not clear how to do that.

Thanks!

I've described a solution in #295 to create a command which will organize the imports. This command can then be used through autocmd to run on file save (BufWritePost). Does this help?

I've described a solution in #295 to create a command which will organize the imports. This command can then be used through autocmd to run on file save (BufWritePost). Does this help?

Thank you. I added the snippet to my configuration in the first post under -- Ruff command to organize imports on save. Is it placed correctly? I'm quite new to Neovim, do not know where/how to set the autocmd if the above is correct.

Instead of formatting on save I started using the following:

["<leader>lf"] = { "<cmd>lua vim.lsp.buf.format({async = true, filter = function(client) return client.name ~= 'typescript-tools' end})<cr>", "Format", }, to format the code. It works but is there a way to also organize the imports with the same key map?

There are two possible solutions to this:

  1. You can either configure an autocmd to run the source.organizeImports.ruff on save like #409 (comment)
  2. Or, use a plugin like conform.nvim which allows you to define your own formatter command. So, you can define a ruff_organize_imports like (you might want to refer to the plugin docs for more info):
require('conform').setup {
  formatters_by_ft = {
    python = { 'ruff_organize_imports' },
  },
  formatters = {
    ruff_organize_imports = {
      command = 'ruff',
      args = {
        'check',
        '--force-exclude',
        '--select=I001',
        '--fix',
        '--exit-zero',
        '--stdin-filename',
        '$FILENAME',
        '-',
      },
      stdin = true,
      cwd = require('conform.util').root_file {
        'pyproject.toml',
        'ruff.toml',
        '.ruff.toml',
      },
    },
  },
}

I'll close this issue as it's not actionable from within ruff-lsp.

I have a keybinding to format:

['<leader>lf'] = {"<cmd>lua vim.lsp.buf.format({async = true, filter = function(client) return client.name ~= 'typescript-tools' end})<cr>", 'Format'

and one for the Code Actions:

['<leader>la'] = { '<cmd>lua vim.lsp.buf.code_action()<cr>', 'Code Action' }

But I have to select manually to sort the imports or fix all.

Is there a direct command I can call to only sort the imports instead?

@steakhutzeee Can you try updating the function invoked by the <leader>la keybinding with the appropriate option to signal Neovim to apply the code actions directly? This is suggested in #409 (comment), I'll put it here for reference:

vim.lsp.buf.code_action {
  context = {
    -- It's important to only ask for a single code action otherwise Neovim will open
	-- up a UI prompt if there are more.
    only = { 'source.fixAll.ruff' },
  },
  -- Inform Neovim to apply the code action edits
  apply = true,
}

@steakhutzeee Can you try updating the function invoked by the <leader>la keybinding with the appropriate option to signal Neovim to apply the code actions directly? This is suggested in #409 (comment), I'll put it here for reference:

vim.lsp.buf.code_action {
  context = {
    -- It's important to only ask for a single code action otherwise Neovim will open
	-- up a UI prompt if there are more.
    only = { 'source.fixAll.ruff' },
  },
  -- Inform Neovim to apply the code action edits
  apply = true,
}

Thanks, I actually managed to apply the following to format and sort imports at the same time on save:

vim.api.nvim_create_autocmd({ "BufWritePost" }, {
  pattern = { "*.py" },
  callback = function()
    vim.lsp.buf.code_action {
      context = {
        only = { 'source.organizeImports.ruff' },
      },
      apply = true,
    }
    vim.lsp.buf.format({async = true, filter = function(client) return client.name ~= 'typescript-tools' end})
    vim.wait(100)
  end,
})

Looks it's working. Do you see any issues with it?

I think it could create a race condition because both vim.lsp.buf.code_action and vim.lsp.buf.format (with async = true) are asynchronous. So, they could, sometimes, write the content to the same file which could lead to undefined behavior. I'm not exactly sure how to solve this as I've been using conform.nvim for my formatting needs.

I think it could create a race condition because both vim.lsp.buf.code_action and vim.lsp.buf.format (with async = true) are asynchronous. So, they could, sometimes, write the content to the same file which could lead to undefined behavior. I'm not exactly sure how to solve this as I've been using conform.nvim for my formatting needs.

I tried your conform configuration but with a little edit because I was also using 'ruff_format'.

    formatters_by_ft = {
      python = {'ruff_organize_imports', 'ruff_format'},
    },

Well it was formatting fine but the imports were not sorted.

Looking at the Conform logs i could see it could not find any ruff command. So i wonder how it was formatting?

Anyway I had to also install ruff with Mason , so I actually have both ruff and ruff-lsp.

Is this correct? Should have both?

In case someone comes here from a search engine:

vim.api.nvim_create_autocmd("FileType", {
    desc = "Comprehensively reformat Python with Ruff",
    pattern = "python",
    callback = function()
        vim.keymap.set('n', 'gF', function()
            vim.lsp.buf.code_action {
                context = { only = { 'source.fixAll' }, diagnostics = {} },
                apply = true,
            }
            vim.lsp.buf.format { async = true }
        end, { desc = 'Format buffer' })
    end
})

Could somebody tell me please then how can i format also imports on save?

return {
    "nvimtools/none-ls.nvim",
    dependencies = {
        "nvimtools/none-ls-extras.nvim",
        "nvim-lua/plenary.nvim",
    },
    config = function()
        local null_ls = require("null-ls")
        null_ls.setup({
            debug = true,
            sources = {
                null_ls.builtins.diagnostics.mypy,
                null_ls.builtins.formatting.stylua,
                require("none-ls.diagnostics.ruff"),
                require("none-ls.formatting.ruff"),
            },
        })
        vim.keymap.set("n", "<leader>gf", vim.lsp.buf.format, {})
        vim.cmd([[autocmd BufWritePre * lua vim.lsp.buf.format()]])
    end,
}

what do add in here?

Could somebody tell me please then how can i format also imports on save?

return {
    "nvimtools/none-ls.nvim",
    dependencies = {
        "nvimtools/none-ls-extras.nvim",
        "nvim-lua/plenary.nvim",
    },
    config = function()
        local null_ls = require("null-ls")
        null_ls.setup({
            debug = true,
            sources = {
                null_ls.builtins.diagnostics.mypy,
                null_ls.builtins.formatting.stylua,
                require("none-ls.diagnostics.ruff"),
                require("none-ls.formatting.ruff"),
            },
        })
        vim.keymap.set("n", "<leader>gf", vim.lsp.buf.format, {})
        vim.cmd([[autocmd BufWritePre * lua vim.lsp.buf.format()]])
    end,
}

what do add in here?

https://github.com/nvimtools/none-ls.nvim/wiki/Formatting-on-save

Lazy: SichangHe/.config@f60646e.

Would be possible to use this in Conform?