2

I am a Python developer and would like to use Neovim as my main code editor. I have managed to configure auto-completion, linting and auto-formatting using lspconfig, mason, null-ls, nvim-cmp and other plugins. However, I am still missing one great feature that I used a lot while coding in PyCharm: using the "context actions", I was able to import a symbol, i.e. a class, function or module, that is currently under my cursor, as shown on the screenshot below:

Calling "Context Actions" to import a class under cursor

However, I am missing this feature in my Neovim setup. I assume that the "code actions" feature of certain language servers should provide me with an option to import a symbol from a certain module, but I have tried several language servers for Python and had no lock. The language servers I have tried are:

  • jedi-language-server;
  • pyright;
  • basedpyright;
  • pylsp.

Here is a screenshot of what I got with jedi-language-server and ruff_lsp installed when calling a "code action" on a "Path" symbol:

No import suggestions on Code Action

In summary, the question is how can I have this "import" feature in my Neovim setup?

1
  • This is probably currently impossible. You could open issues in the language servers and request that feature to make it possible in the future. Commented Apr 28, 2024 at 20:01

2 Answers 2

1

coc-nvim with coc-pyright "just works" out of the box for importing suggestions with the coc "normal" code completion popup. That's from my setup:

enter image description here

After choosing the completion it is auto-added:

enter image description here

It is also documented https://github.com/fannheyward/coc-pyright:

codeActions to add imports,

python.analysis.autoImportCompletions Determines whether pyright offers auto-import completions ...

Sometimes I also call autoimport.


Does it have a feature that is similar to "Import name" from "Context Actions"

I have this in a menu I enter in :CocFzfList that I mapped to `` on my setup:

enter image description here

Sign up to request clarification or add additional context in comments.

2 Comments

Is the coc.nvim only capable of importing something when I apply the code completion? Because I was able to get the same behaviour using nvim-cmp, but the suggestions list is too small (I didn't get the suggestion for Path class from pathlib when typing). Does it have a feature that is similar to "Import name" from "Context Actions" as in PyCharm?
The "auto import" feature is not "smart" enough, I would say., for example it misses Path from pathlib. There are also other imports I miss, I think this should be brought up upstream to pyright project, the ability of language server is not part of coc. Coc gets data from lsp and has nvim bindings.. But the Union List Any from typing work for me. It also I think "learns" from open buffers, so if there is a buffer with from pathlib import Path, I think it sometimes works on other buffers, dunno. But would be nice to add a
1

I found the exact requirement was missing, so I ended up making a plugin on my own.

https://github.com/kiyoon/python-import.nvim

It uses all of the below in order to find the best import statement:

  1. Pre-defined lookup table
  2. Search project import statements
  3. Pyright LSP

The install instruction and usage may change over time (it's an early stage of the plugin) so I'd advise you to check the README for the latest instructions.

The plugin is customisable, for example, you can change the lookup table definitions. That said, this plugin may NOT suit all your requirements. But making such plugin on your own is straightforward.

How to make such plugin on Neovim

1. Identify what you want to import

Let's say you want to import the word under your cursor. You can use a utility function like this in lua:

---Get current word in a buffer
---It is aware of the insert mode (move column by -1 if the mode is insert).
---@param winnr integer?
---@return string
local function get_current_word(winnr)
  winnr = winnr or vim.api.nvim_get_current_win()
  local bufnr = vim.api.nvim_win_get_buf(winnr)

  local line, col, mode
  vim.api.nvim_win_call(winnr, function()
    line = vim.fn.getline "."
    col = vim.fn.col "."
    mode = vim.fn.mode "."
  end)

  if mode == "i" then
    -- insert mode has cursor one char to the right
    col = col - 1
  end
  local finish = line:find("[^a-zA-Z0-9_]", col)
  -- look forward
  while finish == col do
    col = col + 1
    finish = line:find("[^a-zA-Z0-9_]", col)
  end

  if finish == nil then
    finish = #line + 1
  end
  local start = vim.fn.match(line:sub(1, col), [[\k*$]])
  return line:sub(start + 1, finish - 1)
end

Now, you need to convert the word to the import statement. For example, if your cursor is under np, you want to add a statement like import numpy as np. I used a lookup table that has definitions of common imports, so if I don't like the behaviour I can always force it to return what I like. I also used py-tree-sitter to identify all existing import statements in the project, count them and return the number of import statements. This is the example call of the small cli program that does this job.

$ pip install python-import
$ python-import count /path/my-project logging
00002:from my_module import logging
00001:import logging

2. Identify where to import to.

This is very tricky to define and it depends on your project style. For me personally, I'd put them at the end of existing import statements, or if they don't exist, after all docstrings / comments at the beginning of the document.

I used treesitter to identify the current node types (string, comment, etc.), and justify where it belongs. See the functions below:

---Return line after the first comments and docstring.
---It iterates e.g. 50 first lines and obtains treesitter nodes to check the syntax (string or comment)
---@param bufnr integer?
---@param max_lines integer?
---@return integer?
local function find_line_after_module_docstring(bufnr, max_lines)
  bufnr = bufnr or vim.api.nvim_get_current_buf()
  max_lines = max_lines or 200

  local lines = vim.api.nvim_buf_get_lines(bufnr, 0, max_lines, false)
  for i, line in ipairs(lines) do
    local node = vim.treesitter.get_node { pos = { i - 1, 0 } }

    if
      node ~= nil
      and node:type() ~= "comment"
      and node:type() ~= "string"
      and node:type() ~= "string_start"
      and node:type() ~= "string_content"
      and node:type() ~= "string_end"
    then
      return i
    end
  end
  return nil
end

---Find the first import statement in a python file.
---@param bufnr integer?
---@param max_lines integer?
local function find_line_first_import(bufnr, max_lines)
  bufnr = bufnr or vim.api.nvim_get_current_buf()
  max_lines = max_lines or 200

  local lines = vim.api.nvim_buf_get_lines(bufnr, 0, max_lines, false)
  for i, line in ipairs(lines) do
    local node = vim.treesitter.get_node { pos = { i - 1, 0 } }
    if node ~= nil and (node:type() == "import_statement" or node:type() == "import_from_statement") then
      -- additional check whether the node is top-level.
      -- if not, it's probably an import inside a function
      if node:parent():type() == "module" then
        return i
      end
    end
  end
  return nil
end

---Find the last import statement in a python file.
---@param bufnr integer?
---@param max_lines integer?
local function find_line_last_import(bufnr, max_lines)
  bufnr = bufnr or vim.api.nvim_get_current_buf()
  max_lines = max_lines or 200

  local lines = vim.api.nvim_buf_get_lines(bufnr, 0, max_lines, false)
  -- iterate backwards
  for i = #lines, 1, -1 do
    local node = vim.treesitter.get_node { pos = { i - 1, 0 } }
    if node ~= nil and (node:type() == "import_statement" or node:type() == "import_from_statement") then
      -- additional check whether the node is top-level.
      -- if not, it's probably an import inside a function
      if node:parent():type() == "module" then
        return i
      end
    end
  end
  return nil
end

3. Add a line

Now that you know where and what to put, simplely call:

local line_number = 1
local import_statements = { "import numpy as np" }
vim.api.nvim_buf_set_lines(0, line_number - 1, line_number - 1, false, import_statements)

to put the text. You don't have to worry about the duplicates and sorting because isort or ruff will fix those!

4. Make it into one function and add a keymap.

This is just an example of a simple function utilising the above helper functions.

local import_as = { np = "numpy" }

local function add_import_statement()
  local current_word = get_current_word()
  local line_number = find_line_after_module_docstring()  -- always at the top module

  if import_as[current_word] ~= nil then
    local import_statements = { "import " .. import_as[current_word] .. " as " .. current_word }
    vim.api.nvim_buf_set_lines(0, line_number - 1, line_number - 1, false, import_statements)
  end
end

vim.keymap.set({"i", "n"}, "<M-CR>", add_import_statement)

2 Comments

While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - From Review
@Jesuisme OK, I updated it to include instructions on minimum code to perform such action.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.