Posted on

I've been having this love-hate relationship with GitHub Copilot. On one hand, it saves me a lot of typing, especially with the most cumbersome boilerplate code, like tests. On the other hand, it's constantly getting in my way when writing code and even prose. I feel it robs me of my ability to think about what I'm typing. It's just there, constantly trying to be helpful. Not to mention the countless hours I've spent debugging issues caused by its code suggestions. And yet, it's still a very convenient tool. But only sometimes.

The problem is considerably worse when it comes to Neovim, which is my go-to editor for all text/code editing. The best plugin available, at least to my knowledge at the time of writing, is copilot.lua. Since this plugin uses nvim-cmp to show suggestions and this plugin by default uses <Enter> to select a suggestion, it becomes extremely frustrating to press <Enter>, expecting a new line to be inserted and getting a whole function implementation body instead.

To improve this experience I decided to do a couple of tweaks that seem to be working out for me. I'm sharing them here in case someone else is having the same challenges.

First, I decided to add a shortcut to disable Copilot entirely. This doesn't address the issue per se, but it's a quick way to get rid of it when it's being too chatty and I don't need it. I'm now even considering starting it disabled by default. But for now, I just added the following lines to my lua/config/keymaps.lua:

-- copilot
vim.keymap.set("n", "<leader>cpd", ":Copilot disable<cr>", { silent = true,
noremap = true })
vim.keymap.set("n", "<leader>cpe", ":Copilot enable<cr>", { silent = true,
noremap = true })

This is already some improvement, but it's still not quite there. When Copilot is enabled, I still have to deal with the issue of pressing <Enter> and getting a suggestion instead of a new line. So I ended up tweaking the completion engine itself: nvim-cmp. Since I wanted to achieve a behavior similar to VS Code, I followed this Super-Tab recipe from LazyVim, with some changes.

First I changed luasnip configuration to remove the <Tab> key mapping. I added the file lua/plugins/luasnip.lua with the following lines:

return {
  "L3MON4D3/LuaSnip",
  keys = function()
    return {}
  end,
}

Next, I changed the completion engine configuration by adding the file lua/plugins/nvim-cmp.lua with the following lines:

return {
  "hrsh7th/nvim-cmp",
  ---@param opts cmp.ConfigSchema
  opts = function(_, opts)
    local has_words_before = function()
      unpack = unpack or table.unpack
      local line, col = unpack(vim.api.nvim_win_get_cursor(0))
      return col ~= 0 and vim.api.nvim_buf_get_lines(0, line - 1, line, true)[1]:sub(col, col):match("%s") == nil
    end

    local luasnip = require("luasnip")
    local cmp = require("cmp")

    opts.mapping = vim.tbl_extend("force", opts.mapping, {
      ["<Tab>"] = cmp.mapping(function(fallback)
        if cmp.visible() then
          cmp.confirm({ select = true })
        elseif luasnip.expand_or_jumpable() then
          luasnip.expand_or_jump()
        elseif has_words_before() then
          cmp.complete()
        else
          fallback()
        end
      end, { "i", "s" }),
      ["<S-Tab>"] = cmp.mapping(function(fallback)
        if cmp.visible() then
          cmp.select_prev_item()
        elseif luasnip.jumpable(-1) then
          luasnip.jump(-1)
        else
          fallback()
        end
      end, { "i", "s" }),
      ["<CR>"] = cmp.config.disable,
    })
  end,
}

The changes I've made to the recipe were basically to make <Tab> confirm the selection when completion panel is open, and disable the <Enter> key mapping so that it has no action.

As I already mentioned, these improvements are working out pretty well for me, giving me more control when using Copilot and making it more enjoyable to use in Neovim.