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

NOTE: The following map has a tradeoff, because you lose the hability to precede insert mode entering with a count. In this case I advise you to remember to enter insert mode using S to adjust the indentation automatically instead of using the below mapping. You could add a message in the mapping until you learn the lesson.

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

Now if the user uses v.count or try to record a macro i is used.

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)