DEV Community

Cover image for Neovim smart mappings
Sérgio Araújo
Sérgio Araújo

Posted on • Edited on

Neovim smart mappings

Intro

Smart mappings are mappings that have their behavior adapted to the context.

This post, I hope, will be improved but for now I want to give you a couple of examples. If you want you can access my nvim config here

For mappings that use Utils, have a look at the link above and use:

local Utils = require('core.utils')
Enter fullscreen mode Exit fullscreen mode

NOTE: There is a convention for most lua developpers, when we create a module it goes like this:

local M = {}

M.my_task = function()
  code
end

return M
Enter fullscreen mode Exit fullscreen mode

Where M is a mnemonic for module.

This makes clear the purpose of some M.stuff we see here and there and it will come from a required (imported) module.

Why so many mappings and settings?

tjdevris developer of telescope among others uses the term PDE (Personal Development Environment) in oposition to IDE because advanced nvim users usually have many plugins and specific mappings for their needs. I think a good PDE could help you more.

Every time I face a boring task I start thinking on how can I modify my nvim config to make it easier

Function for mappings

-- https://blog.devgenius.io/create-custom-keymaps-in-neovim-with-lua-d1167de0f2c2
-- https://oroques.dev/notes/neovim-init/
M.map = function(mode, lhs, rhs, opts)
  local options = { noremap = true }
  if opts then
    options = vim.tbl_extend('force', options, opts)
  end
  vim.keymap.set(mode, lhs, rhs, options)
end
Enter fullscreen mode Exit fullscreen mode

If the user does not define opts it will use only options noremap = true otherwise it will extend the options table. The lhs means left hand side, your keybind, the rhs means right hand side, your action.

The map function comes from the file ~/.config/nvim/lua/core/utils.lua

So I can do:

local map = require("core.utils").map
Enter fullscreen mode Exit fullscreen mode

Toggle checkboxes

This mapping happens in three stages:

  1. Grab current line content
  2. Toggle [ ] or [x]
  3. Set the line back
-- inspiration from this thead:
-- https://www.reddit.com/r/neovim/comments/10s5oou
vim.keymap.set(
  {'n','i' },
  '<leader>tt',
  function()
    local line = vim.api.nvim_get_current_line()
    local modified_line = line:gsub("(- %[)(.)(%])",
      function(prefix, checkbox, postfix)
        checkbox = (checkbox == " ") and "x" or " "
        return prefix .. checkbox .. postfix
    end)
    vim.api.nvim_set_current_line(modified_line)
  end,
  {
    desc = 'Ftplugin - Toggle checkboxes',
    buffer = true,
  }
)
Enter fullscreen mode Exit fullscreen mode

Here is the main part of the above mapping:

local modified_line = line:gsub("(- %[)(.)(%])",
function(prefix, checkbox, postfix)
  checkbox = (checkbox == " ") and "x" or " "
  return prefix .. checkbox .. postfix
end)
Enter fullscreen mode Exit fullscreen mode

The trick here happens when we get the value of the current line "line"
in our case and use the gsub method to change it. The regular expression has three groups:

(- %[)  .... dash followed by [
(.) ........ any character "either x or space"
(%]) ......... literal ]
Enter fullscreen mode Exit fullscreen mode

These three groups become "prefix", "checkbox" and "postfix"

TIP: The % is used to scape the next character given us its literal version.

The mapping for toggling checkboxes goes into after/ftplugin/markdown.lua because it does not make sense in other contexts, and that's why I do not use the function map here.

Next line in lists ...

Here how we can make markdown lists smarter, if you are in a list the next line automatically will copy the above pttern either "-", "+", "*", "- [ ]" and so on...

local function is_in_list()
  local current_line = vim.api.nvim_get_current_line()
  return current_line:match('^%s*[%*-+]%s') ~= nil
end

local function has_checkbox()
  local current_line = vim.api.nvim_get_current_line()
  return current_line:match('%s*[%*-+]%s%[[ x]%]') ~= nil
end

local function list_prefix()
  local line = vim.api.nvim_get_current_line()
  local list_char = line:gsub("^%s*([-%*+] )(.*)",
    function(prefix, rest)
      return prefix
    end)
  return list_char
end

local function is_in_num_list()
  local current_line = vim.api.nvim_get_current_line()
  return current_line:match('^%s*%d+%.%s') ~= nil
end

vim.keymap.set('i', '<cr>', function()
  if is_in_list() then
    local prefix = list_prefix()
    return has_checkbox() and '<cr>' .. prefix .. '[ ] ' or '<cr>' .. prefix
  elseif is_in_num_list() then
    local line = vim.api.nvim_get_current_line()
    local modified_line = line:gsub("^%s*(%d+)%.%s.*$",
      function(numb)
        numb = tonumber(numb) + 1
        return tostring(numb)
      end)
    return '<cr>' .. modified_line .. '. '
  else
    return '<cr>'
  end
end, {
  buffer = true,
  expr = true,
})

vim.keymap.set(
  'n',
  'o',
  function()
    if is_in_list() then
      local prefix = list_prefix()
      return has_checkbox() and 'o' .. prefix .. '[ ] ' or 'o' .. prefix
  elseif is_in_num_list() then
    local line = vim.api.nvim_get_current_line()
    local modified_line = line:gsub("^%s*(%d+)%.%s.*$",
      function(numb)
        numb = tonumber(numb) + 1
        return tostring(numb)
      end)
    return 'o' .. modified_line .. '. '
    else
      return 'o'
    end
  end, {
    buffer = true,
    expr = true,
  })

vim.keymap.set('n', 'O', function()
  if is_in_list() then
    local prefix = list_prefix()
    return has_checkbox() and 'O' .. prefix .. '[ ] ' or 'O' .. prefix
  elseif is_in_num_list() then
    local line = vim.api.nvim_get_current_line()
    local modified_line = line:gsub("^%s*(%d+)%.%s.*$",
      function(numb)
        numb = tonumber(numb) + 1
        return tostring(numb)
      end)
    return 'O' .. modified_line .. '. '
  else
    return 'O'
  end
end, {
  buffer = true,
  expr = true,
})

Enter fullscreen mode Exit fullscreen mode

Smart gf

In this case we are gonna try to run gf "go to file" throught a protected call (kind of try catch in lua) otherwise we are gonna use the default behavior of Enter

map(
  'n',
  '<CR>',
  function()
    if not pcall(vim.cmd.normal, 'gf') then
      vim.cmd.normal('j0')
    end
  end,
  {
    desc = 'gf or enter',
    noremap = true,
    silent = true,
  }
)
Enter fullscreen mode Exit fullscreen mode

gx that opens github repos

When we are reading someone's neovim config files, it happens all the time to me, we usully stumble upon plugins we do not have any idea of what they do, so we need to open that repo to figure out its purpose, hence this mapping.

-- in utils I have
M.is_mac = function()
  return vim.loop.os_uname().sysname == "Darwin"
end
Enter fullscreen mode Exit fullscreen mode

In the code bellow we are using a kind of ternary trick in lua

local open_command = (Utils.is_mac() == true and 'open') or 'xdg-open'

local function url_repo()
  local cursorword = vim.fn.expand('<cfile>')
  if string.find(cursorword, '^[a-zA-Z0-9-_.]*/[a-zA-Z0-9-_.]*$') then
    cursorword = 'https://github.com/' .. cursorword
  end
  return cursorword or ''
end

map(
  'n',
  'gx',
  function()
    vim.fn.jobstart({ open_command, url_repo() }, { detach = true })
  end,
  {
    silent = true,
    desc = 'xdg open link',
  }
)
Enter fullscreen mode Exit fullscreen mode

Smart dd

The function to test empty lines

M.is_empty_line = function()
  local current_line = vim.api.nvim_get_current_line()
  return current_line:match('^%s*$') ~= nil
end
Enter fullscreen mode Exit fullscreen mode

In this case we are going to discard empty lines sending them to the black hole register "_

map(
  'n',
  'dd',
  function()
    return Utils.is_empty_line() and '"_dd' or 'dd'
  end,
  {
    expr = true,
    desc = "delete blank lines to black hole register",
  }
)
Enter fullscreen mode Exit fullscreen mode

Smart indent in insert mode

When you start insert mode using 'S' vim/neovim will indent by context. What we are doing here is to make sure, even if you forget using "S" to start insert properlly, that neovim will give you a hand and take the right decision for you.

map(
  "n",
  "i",
  function()
    return Utils.is_empty_line() and 'S' or 'i'
  end,
  {
    expr = true,
    desc = "properly indent on empty line when insert",
  }
)
Enter fullscreen mode Exit fullscreen mode

NOTE: When your map returns either one or another thing you must use expr = true in the options table like you see in above example.

Fix spell mistakes in insert mode

#Insert mode fix last mispelled word:

vim.keymap.set(
   'i',
   '<M-1>',
   function()
    return '<Esc>[s1z=gi'
  end,
   {
     desc = 'Fix spell',
     silent = true,
     expr = true,
   }
)
Enter fullscreen mode Exit fullscreen mode

Top comments (0)