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')
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
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
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
Toggle checkboxes
This mapping happens in three stages:
- Grab current line content
- Toggle
[ ]
or[x]
- 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,
}
)
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)
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 ]
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,
})
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,
}
)
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
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',
}
)
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
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",
}
)
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",
}
)
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,
}
)
Top comments (0)