astral-sh / ruff-lsp

A Language Server Protocol implementation for Ruff.
Other
1.3k stars 46 forks source link

Enable import autosort on save #387

Closed steakhutzeee closed 6 months ago

steakhutzeee commented 8 months ago

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!

dhruvmanila commented 8 months ago

I've described a solution in https://github.com/astral-sh/ruff-lsp/issues/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?

steakhutzeee commented 8 months ago

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.

steakhutzeee commented 8 months ago

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?

dhruvmanila commented 6 months ago

There are two possible solutions to this:

  1. You can either configure an autocmd to run the source.organizeImports.ruff on save like https://github.com/astral-sh/ruff-lsp/issues/409#issuecomment-2069037437
  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.

steakhutzeee commented 6 months ago

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?

dhruvmanila commented 6 months ago

@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 https://github.com/astral-sh/ruff-lsp/issues/409#issuecomment-2069037437, 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 commented 6 months ago

@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?

dhruvmanila commented 6 months ago

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.

steakhutzeee commented 6 months ago

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?

savchenko commented 3 months ago

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
})
SichangHe commented 3 months ago

Lazy: https://github.com/SichangHe/.config/commit/f60646ee896dfc835c8e8f0d60ef5e01fc0a406f.

KamilKleina commented 3 months ago

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?

steakhutzeee commented 2 months ago

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

steakhutzeee commented 2 months ago

Lazy: SichangHe/.config@f60646e.

Would be possible to use this in Conform?