Neovim smart mappings
Tags: nvim, productivity, programming
Published: 2024-03-07 13:17:57

# 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](https://bitbucket.org/sergio/mylazy-nvim/src/master/)

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

```lua
local Utils = require('core.utils')
```

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

```lua
local M = {}

M.my_task = function()
  code
end

return M
```

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](https://github.com/tjdevries) 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

```lua
-- 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
```

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:

```lua
local map = require("core.utils").map
```

## Toggle checkboxes

This mapping happens in three stages:

1. Grab current line content
2. Toggle `[ ]` or `[x]`
3. Set the line back

```lua
-- 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,
  }
)
```
Here is the main part of the above mapping:

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

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:

```txt
(- %[)  .... dash followed by [
(.) ........ any character "either x or space"
(%]) ......... literal ]
```

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...

```lua
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,
})

```

## 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

```lua
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,
  }
)
```

## 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.

```lua
-- in utils I have
M.is_mac = function()
  return vim.loop.os_uname().sysname == "Darwin"
end
```

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

```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',
  }
)
```

## Smart dd

The function to test empty lines

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

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

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

## 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. 

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

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.