Utility library to n^x your work with the nvim api.
Features
Installation
All features maintain familiarity with their underlying base functions. Below is an overview of their differences and extended functionalities.
Based on vim.keymap.set()
vim.keymap.set('n', ';w', "<Cmd>w<CR>")
vim.keymap.set('n', ';q', "<Cmd>w<CR>", { desc = "Close Current Window" })
vim.keymap.set('', "j", "&wrap ? 'gj' : 'j'", { expr = true, silent = true })
vim.keymap.set('', "<Down>", "&wrap ? 'gj' : 'j'", { expr = true, silent = true })
-- Can be written as
nx.map({
   { ";w", "<Cmd>w<CR>" },
   { ";q", "<Cmd>confirm quit<CR>", desc = "Close Current Window" },
   { { "j", "<Down>" }, "&wrap ? 'gj' : 'j'", "", expr = true, silent = true },
})
-- Options like filetype and wk_label are not directly supported by vim.keymap.set
nx.map({
   { "<leader>ts", "<Cmd>set spell!<CR>", desc = "Toggle Spellcheck", wk_label = "Spellcheck" },
   { "<leader>tp", "<Cmd>MarkdownPreviewToggle<CR>", ft = "markdown", desc = "Toggle Markdown Preview" },
})
-- Wrapper options
nx.map({
   -- Line Navigation
   { { "j", "<Down>" }, "&wrap ? 'gj' : 'j'", "" },
   { { "k", "<Up>" }, "&wrap ? 'gk' : 'k'", "" },
   { "$", "&wrap ? 'g$' : '$'", "" },
   { "^", "&wrap ? 'g^' : '^'", "" },
   -- Enter insert mode at indentation level on empty lines
   { "i", "len(getline('.')) == 0 ? '\"_cc' : 'i'"  },
   }, { expr = true, silent = true })
})
Differences
[1]: lhs and [2]: rhsmode: defaults to "n" and instead of being passed as index [1] it is optional as [3] or mode keyopts: are passed inline instead of in a separate tablewk_label: to create which-key labels that should differ from the key's descriptionft: to create filetype specific mappings{<wrapper_opts>}: to add options to all keymaps within a nx.map()nx.map({ ";q", "<Cmd>confirm quit<CR>", desc = "Close Current Window" })
---@ ╰── set a single keymap
---@ ╭── or lists of keymaps
nx.map({
   -- Line Navigation
   ---@    ╭── multiple lhs
   { { "j", "<Up>" }, "&wrap ? 'gj' : 'j'", "" },
   { { "k", "<Down>" }, "&wrap ? 'gk' : 'k'", "" },
   { "$", "&wrap ? 'g$' : '$'", "" },
   { "^", "&wrap ? 'g^' : '^'", "" },
   -- Indentation
   { "i", function() return smart_indent "i" end },
   { "a", function() return smart_indent "a" end },
   { "A", function() return smart_indent "A" end },
   }, { expr = true, silent = true })
---@      ╰── wrapper opts apply options to all entries
nx.map({
   { "<Esc>", "<Esc>", "i" },
   { "<C-c>", "<Cmd>close<CR>", { "i", "x" } },
   { "q", "<Cmd>close<CR>", "x" },
   ---@ set filetype keymaps ──╮ (in {wrapper_opts} or for single keymaps)
}, { buffer = 0, ft = "DressingInput" })
Specify a mode|mode[] other than "n" as index [3] or mode key (inline or in wrapper_opts).
nx.map({
{ "<kEnter>", "<CR>", { "", "!" }, desc = "Enter" }
---@ ^=                      ╰── or  ──╮
{ "<kEnter>", "<CR>", desc = "Enter", mode = { "", "!" } }
}, { mode = { "", "!" }) -- or in wrapper_opts (here it has to be the `mode` key)
Add options to all entries in a list of keymaps - inline keymap options are treated with higher priority and won't be overwritten by wrapper options. As an example, let's compare nx.map for setting neovim/nvim-lspconfig#suggested-configuration.
---@ reference using the default vim.keymap.set
local opts = { noremap=true, silent=true }
vim.keymap.set('n', '<space>e', vim.diagnostic.open_float, opts)
vim.keymap.set('n', '[d', vim.diagnostic.goto_prev, opts)
vim.keymap.set('n', ']d', vim.diagnostic.goto_next, opts)
vim.keymap.set('n', '<space>q', vim.diagnostic.setloclist, opts)
local on_attach = function(client, bufnr)
   local bufopts = { noremap=true, silent=true, buffer=bufnr }
   vim.keymap.set('n', 'gD', vim.lsp.buf.declaration, bufopts)
   vim.keymap.set('n', 'gd', vim.lsp.buf.definition, bufopts)
   vim.keymap.set('n', 'K', vim.lsp.buf.hover, bufopts)
   vim.keymap.set('n', 'gi', vim.lsp.buf.implementation, bufopts)
   vim.keymap.set('n', '<C-k>', vim.lsp.buf.signature_help, bufopts)
   vim.keymap.set('n', '<space>wa', vim.lsp.buf.add_workspace_folder, bufopts)
   vim.keymap.set('n', '<space>wr', vim.lsp.buf.remove_workspace_folder, bufopts)
   vim.keymap.set('n', '<space>wl', function()
      print(vim.inspect(vim.lsp.buf.list_workspace_folders()))
   end, bufopts)
   vim.keymap.set('n', '<space>D', vim.lsp.buf.type_definition, bufopts)
   vim.keymap.set('n', '<space>rn', vim.lsp.buf.rename, bufopts)
   vim.keymap.set('n', '<space>ca', vim.lsp.buf.code_action, bufopts)
   vim.keymap.set('n', 'gr', vim.lsp.buf.references, bufopts)
   vim.keymap.set('n', '<space>f', function() vim.lsp.buf.format { async = true } end, bufopts)
end
---@ nx.map
nx.map({
   { "<space>e", vim.diagnostic.open_float },
   { "[d", vim.diagnostic.goto_prev },
   { "]d", vim.diagnostic.goto_next },
   { "<space>q", vim.diagnostic.setloclist },
}, { noremap = true, silent = true })
local on_attach = function(client, bufnr)
   nx.map({
      { "gD", vim.lsp.buf.declaration },
      { "gd", vim.lsp.buf.definition },
      { "K", vim.lsp.buf.hover },
      { "gi", vim.lsp.buf.implementation },
      { "<C-k>", vim.lsp.buf.signature_help },
      { "<space>wa", vim.lsp.buf.add_workspace_folder },
      { "<space>wr", vim.lsp.buf.remove_workspace_folder },
      { "<space>wl", function() print(vim.inspect(vim.lsp.buf.list_workspace_folders())) end },
      { "<space>D", vim.lsp.buf.type_definition },
      { "<space>rn", vim.lsp.buf.rename },
      { "<space>ca", vim.lsp.buf.code_action },
      { "gr", vim.lsp.buf.references },
      { "<space>f", function() vim.lsp.buf.format { async = true } end },
   }, { noremap = true, silent = true, buffer = bufnr })
end
Register which-key labels that should differ from the mappings' description.
They can be a string literal, "ignore"(^="which_key_ignore"), or { sub_desc = "<pattern>" } to exclude a pattern of the mappings desc key.
The example below uses wk_label for a "SnipRun-keymap-family". It excludes the string "SnipRun" from being added to every entry on their which-key page.
---@ method 1: a custom `wk_label` per key
nx.map({
   { "<leader>Rc", "<Cmd>SnipClose<CR>", desc = "Close SnipRun", wk_label = "Close" },
   { "<leader>Rf", "<Cmd>%SnipRun<CR>", desc = "Run File" },
   { "<leader>Ri", "<Cmd>SnipInfo<CR>", desc = "SnipRun Info", wk_label = "Info" },
   { "<leader>Rm", "<Cmd>SnipReplMemoryClean<CR>", desc = "SnipRun Clean Memory", wk_label = "Clean Memory" },
   { "<leader>Rr", "<Cmd>SnipReset<CR>", desc = "Reset SnipRun", wk_label = "Reset" },
   { "<leader>Rx", "<Cmd>SnipTerminate<CR>", desc = "Terminate SnipRun", wk_label = "Terminate" },
   { "<leader>R", "<Esc><Cmd>'<,'>SnipRun<CR>", "v", desc = "SnipRun Range", wk_label = "Run Range" },
})
---@ method 2: use `sub_desc` in `wrapper_opts` to remove `SnipRun` from all entries
nx.map({
   { "<leader>Rc", "<Cmd>SnipClose<CR>", desc = "Close SnipRun" },
   { "<leader>Rf", "<Cmd>%SnipRun<CR>", desc = "Run File" },
   { "<leader>Ri", "<Cmd>SnipInfo<CR>", desc = "SnipRun Info" },
   { "<leader>Rm", "<Cmd>SnipReplMemoryClean<CR>", desc = "SnipRun Clean Memory" },
   { "<leader>Rr", "<Cmd>SnipReset<CR>", desc = "Reset SnipRun" },
   { "<leader>Rx", "<Cmd>SnipTerminate<CR>", desc = "Terminate SnipRun" },
   { "<leader>RR", "<Esc><Cmd>'<,'>SnipRun<CR>", "v", desc = "SnipRun Range" },
}, { wk_label = { sub_desc = "SnipRun" } })
Allow your language server to assist you with hovers, completions, and diagnostics.
This requires your runtime environment to be configured to include your plugin directories. An easy way is to have this automated using folke/neodev.nvim.
Based on nvim_set_hl()
nx.hl({
 { "LineNr", fg = "DraculaComment:fg" },
 { "Normal", bg = "DraculaBg:bg" },
 { "BgDarker", bg = palette.bg .. ":#b-15" },
 { "BufferLineSeparatorShadow", fg = "TabLine:bg:#b-10", bg = "Normal:bg" } }
 { { "Directory", "MarkSign" }, link = "DraculaPurple" },
})
Differences
[1]: hl_name and bg|fg|link|…: valuens_id: defaults to 0 and instead of being passed as index [1] it is optional as [3] or ns_id keyvalues: are passed inline instead of in a separate table:bg|:fg: to use single values of other highlights as color source instead of linking the whole group.:#b: to transform the brightness of a color{<wrapper_opts>} to add values to all highlights within a nx.hl()nx.hl({ "GitSignsCurrentLineBlame", fg = "Debug:fg", bg = "CursorLine:bg", italic = true })
---@ ╰── set a single highlight
---@ ╭── or lists of highlights
nx.hl({
   { "Hex", fg = "#9370DB" },            --   ╮
   { "ColorName", fg = "MediumPurple" }, ---@ ├  kinds of values already possible without nx.nvim
   { "Decimal", fg = 9662683 },          --   ╯
   --
   { "Winbar", fg = "DraculaComment:fg" },
   ---@                       ╭────╯  use single values from other highlight groups
   { "Normal", bg = "DraculaBg:bg" },
   ---@ use a color with transformed brightness  ──╮ ╭─ darken
   { "BufferLineSeparatorShadow", fg = "TabLine:bg:#b-10", bg = "Normal:bg" } }
   ---@ e.g., with hex var ──╮         ╭─ brighten
   { "BgLight", bg = palette.bg .. ":#b+15" },
   ---@           ╭── multiple highlight names
   { { "Directory", "MarkSign" }, link = "DraculaPurple" },
}, { bold = true, italic = true })
---@    ╰── wrapper opts apply values to all entries
Add values to all entries in a list of highlights - inline values are treated with higher priority and won't be overwritten by wrapper options.
nx.hl({
   { "NeoTreeTabActive", bg = "NeoTreeNormal:bg" },
   { "NeoTreeTabInactive", fg = "NeoTreeDimText:fg" },   -- ╮
   { "NeoTreeTabSeparatorInactive", fg = "TabLine:bg" }, -- ┤
}, { bg = "TabLine:bg" }) ---@    applies `bg` these     -- ╯
Based on nvim_create_autocmd()
nx.au({
   { "BufWritePost", pattern = "options.lua", command = "source <afile>", desc = "Execute files on save" },
   { "BufWritePre", command = "call mkdir(expand('<afile>:p:h'), 'p')", desc = "Create non-existent parents" },
})
nx.au({
   { "BufWinLeave", pattern = "*.*", command = "mkview" },
   { "BufWinEnter", pattern = "*.*", command = "silent! loadview" },
}, { create_group = "RememberFolds" })
Differences
opts: are passed inline instead of in a separate tablecreate_group: to create a group and add the autocmd|autocmd[] to that group{<wrapper_opts>} to add values to all autocmds within a nx.au()nx.au({ "FocusGained", pattern = "*.*", command = "checktime", desc = "Check if buffer changed outside of vim" })
---@ ╰── create a single autocommand
---@ ╭── or lists of autocommands
nx.au({
   { "BufWinLeave", pattern = "*.*", command = "mkview" },
   { "BufWinEnter", pattern = "*.*", command = "silent! loadview" },
}, { pattern = "*.*" })
---@     ╰── wrapper opts apply values to all entries without them
Besides the usual adding to existing autocommand groups using the group key, it is possible to create autocommand groups on the fly with the create_group key.
nx.au({
   "BufWritePre",
   -- group = "FormatOnSave", ---@ use `group` as usual to add the autocmd to an already existing group
   create_group = "FormatOnSave", ---@ or create a new group while creating the autocmd
   callback = function()
      if next(vim.lsp.get_active_clients({ bufnr = 0 })) == nil then return end
      vim.lsp.buf.format({ async = false })
   end,
})
nx.au({
   { "BufWinLeave", pattern = "*.*", command = "mkview" },
   { "BufWinEnter", pattern = "*.*", command = "silent! loadview" },
   ---@   ╭── create an autocommand group in `wrapper_opts` and add all autocommands within this "nx.au()" call
}, { create_group = "RememberFolds" })
Based on nvim_create_user_command()
nx.cmd({
  "LspFormat",
  function() vim.lsp.buf.format({ async = true }) end,
  bang = true,
  desc = "Fromat the Current Buffer",
})
Differences
[1]: name and [2]: commandopts: are passed inline instead of in a separate table{<wrapper_opts>} to add values to all commands within a nx.cmd()---@ ╭── create a single command
nx.cmd({ "ResetTerminal", function() vim.cmd("set scrollback=1 | sleep 10m | set scrollback=10000") end })
---@ ╭── or lists of commands
nx.cmd({
   { "LspFormat", function() vim.lsp.buf.format({ async = true }) end },
   { "LspToggleAutoFormat", function(opt) toggle_format_on_save(opt.args) end, nargs = "?" },
   { "ToggleBufferDiagnostics", function() toggle_buffer_diags(vim.fn.bufnr()) end },
}, { bang = true })
---@   ╰── wrapper opts apply options to all entries without them
There is also nx.set to assign multiple variables or options.
Next to an array of variables/settings, add the scope (vim.g|vim.opt|vim.bo|...) as a second parameter. If no scope is specified vim.g is used.
(This features function currently consists of just over 10 lines of code. It's not as extensive or well annotated, but feel free to use it if you like).
Variables
nx_set({
   dracula_italic = 1,
   dracula_bold = 1,
   dracula_full_special_attrs_support = 1,
   dracula_colorterm = 0,
})
-- common way:
vim.g.dracula_italic = 1
vim.g.dracula_bold = 1
vim.g.dracula_full_special_attrs_support = 1
vim.g.dracula_colorterm = 0
Options
nx.set({
   -- General
   clipboard = "unnamedplus", -- use system clipboard
   mouse = "a", -- allow mouse in all modes
   showmode = false, -- print vim mode on enter
   termguicolors = true, -- set term gui colors
   timeoutlen = 350, -- time to wait for a mapped sequence to complete
   fillchars__append = [[eob: ,fold: ,foldopen:,foldsep: ,foldclose:›, vert:▏]],
   listchars__append = [[space:⋅, trail:⋅, eol:↴]],
   -- Auxiliary files
   undofile = true, -- enable persistent undo
   backup = false, -- create a backup file
   swapfile = false, -- create a swap file
   -- Command line
   cmdheight = 0,
   -- Completion menu
   pumheight = 14, -- completion popup menu height
   shortmess__append = "c", -- don't give completion-menu messages
   -- Gutter
   number = true, -- show line numbers
   numberwidth = 3, -- number column width - default "4"
   relativenumber = true, -- set relative line numbers
   signcolumn = "yes:2", -- use fixed width signcolumn - prevents text shift when adding signs
   -- Search
   hlsearch = true, -- highlight matches in previous search pattern
   ignorecase = true, -- ignore case in search patterns
   smartcase = true, -- use smart case
   -- ...
}, vim.opt)
Install "tenxsoydev/nx.nvim" via your favorite plugin manager.
The only thing left to do then is to import the nx functions you want to use.
Set it once as global variable, so it can be called anywhere in a configuration
-- if using a global variable, make sure it's set where it will be loaded before it's used in another place
_G.nx = require("nx")
-- use anywhere
nx.map({})
nx.au({})
nx.hl({})
-- E.g., when using a plugin manger like lazy, add a high priority
require("lazy").setup({
  -- ...
  { "tenxsoydev/nx.nvim", priority = 100, config = function() _G.nx = require "nx" end },
  -- ...
})
-- or if you prefer not to have the `nx` branding, use another vairable name
_G.v = require("nx")
-- use anywhere
v.map({})
v.au({})
v.hl({})
It's also possible to import single modules on demand
local map = require("nx.map")
local hl = require("nx.hl")
local au = require("nx.au")
To be easily composable, the utilities are written as single modules that can stand on their own. So if they can be helpful within the project you are working - and adding dependencies is too heavy - they are light copy pasta 🍝. In such cases, keeping a small reference of attribution warms the heart of your fellow developer.
There is always room for enhancement. Reach out if you experience any issues, would like to request a feature, or submit improvements. If you would like to tackle open issues - they are usually "help wanted" by nature. Leaving an emoji to show support for an idea that has already been requested also helps to prioritize community needs.