Introducción

Esta guía va sobre registrar todas las configuraciones en las que he podido ir trabajando de manera orgánica por algún tiempo.

Esta guía pretende:

  • Ser sencilla
  • Ser mínima
  • Si es posible, configuraciones por defecto

Creamos nuestro esquema de directorios y nos ubicamos en ~/.config/nvim/:

mkdir -p ~/.config/nvim/lua/{config,plugins}
cd ~/.config/nvim/

config/diagnostics

Configura lo que pasará cuando un LSP detecte errores

-- ~/.config/nvim/lua/config/diagnostics.lua

-- Configuración básica de diagnósticos (En mi caso, noice manejará la presentación)
vim.diagnostic.config({
  virtual_text = {
    format = function(diagnostic)
      local icons = { "󰅚", "󰀪", "󰋽", "󰌶" }
      return string.format("%s %s", icons[diagnostic.severity], diagnostic.message)
    end,
  },
  signs = {
    text = { "󰅚", "󰀪", "󰋽", "󰌶" },
  },
  severity_sort = true,
})

config/folding.lua

Algunos lo llaman por su traducción al español, “pliegues”

-- ~/.config/nvim/lua/config/folding.lua
-- Configuración simple de folding con treesitter

-- Configuración mínima
vim.opt.foldmethod = 'expr'
vim.opt.foldexpr = 'nvim_treesitter#foldexpr()'
vim.opt.foldlevel = 99      -- Empezar con todo abierto

config/options

La mayoría son bastante descriptivas de lo que hacen, y es recomendable tomarse el tiempo de revisarlas para asegurar que son de nuestro gusto. Mi idea fue tomar de base el comportamiento que se suele esperar de vim.

-- ~/.config/nvim/lua/config/options.lua

-- Opciones generales de Neovim (Que incluso podrían estar en vim)
vim.opt.title = true                -- Busca cambiar el titulo de la ventana en que la que se ejecuta nvim
vim.opt.titlestring = 'nvim - %{expand("%:t")}' 
vim.opt.mouse = "a"                 -- Habilita el soporte del ratón en todos los modos
vim.opt.number = true               -- Muestra números de línea
vim.opt.cursorline = true           -- Resalta la línea en la que nos encontramos
vim.opt.relativenumber = true       -- Muestra números de línea relativos

-- Identación (Otra configuración básica, de toda la vida)
vim.opt.tabstop = 4
vim.opt.shiftwidth = 4
vim.opt.expandtab = true
vim.opt.smartindent = true
vim.opt.autoindent = true

-- Busqueda
vim.opt.hlsearch = true             -- Resalta todas las coincidencias de búsqueda
vim.opt.incsearch = true            -- Muestra las coincidencias de búsqueda a medida que se escribe
vim.opt.ignorecase = true           -- Ignora mayúsculas/minúsculas en la búsqueda
vim.opt.smartcase = true            -- No ignora mayúsculas/minúsculas si la búsqueda contiene mayúsculas

-- Configuración visual
vim.opt.termguicolors = true        -- Habilita true color, esencial para temas modernos
vim.opt.signcolumn = "yes"          -- Siempre muestra un espacio en la columna izquierda para indicadores visuales, lo cual es preferible para consistencia visual 
vim.opt.showtabline = 2             -- Siempre muestra una tab, lo cual es preferible para consistencia visual
--vim.opt.lazyredraw = true         -- No redibuja durante macros. No activar si se usará noice
--  NOTA: Las siguientes configuraciones implican el uso de algo como snacks / noice + lualine, u otros plugins similares 
vim.opt.cmdheight = 0               -- Eliminamos el área de cmdline. Implica el uso de lualine y de algo como noice, para manejar mensajes
vim.opt.cmdwinheight = 1            -- Para usar en conjunto con cmdheight

-- Configuración de comportamiento
vim.opt.clipboard = "unnamedplus"   -- Permite copiar/pegar desde/hacia el portapapeles del sistema
vim.opt.splitright = true           -- Abre nuevas ventanas divididas a la derecha
vim.opt.splitbelow = true           -- Abre nuevas ventanas divididas abajo
vim.opt.wrap = false                -- Deshabilita el ajuste de línea
vim.opt.scrolloff = 5               -- Mantiene 8 líneas de contexto al desplazarse
vim.opt.sidescrolloff = 5           -- Mantiene 8 columnas de contexto al desplazarse horizontalmente
vim.opt.updatetime = 300            -- Tiempo en ms para escribir el archivo swap y disparar eventos CursorHold
vim.opt.undofile = true             -- Habilita el historial de deshacer persistente
vim.opt.undodir = vim.fn.stdpath("data").. "/undodir" -- Directorio para guardar archivos de undo

-- Sobre el autocompletado en cmd 
vim.opt.wildmenu = true
vim.opt.wildoptions = "pum"         -- popup menu + fuzzy matching
vim.opt.wildmode = { "longest:full", "full" }
vim.opt.completeopt = 'menu,menuone,noselect'

-- Para que recuerde donde estaba abierto tal archivo
vim.api.nvim_create_autocmd("BufReadPost", {
  callback = function()
    local mark = vim.api.nvim_buf_get_mark(0, '"')
    local lcount = vim.api.nvim_buf_line_count(0)
    if mark[1] > 0 and mark[1] <= lcount then
      pcall(vim.api.nvim_win_set_cursor, 0, mark)
    end
  end,
})

-- Los siguientes podrían usarse si no se quiere usar noice o lualine, 
-- vim.api.nvim_create_autocmd("RecordingEnter", {
--   callback = function()
--     vim.notify("Recording @" .. vim.fn.reg_recording())
--   end,
-- })
-- 
-- vim.api.nvim_create_autocmd("RecordingLeave", {
--   callback = function()
--     vim.notify("Recording stopped")
--   end,
-- })

config/keymaps

-- ~/.config/nvim/lua/config/keymaps.lua

local map = vim.keymap.set

-- Para usar con diágnostico
map("n", "<leader>dd", vim.diagnostic.open_float, { desc = "Mostrar diagnóstico" })
map("n", "{d", vim.diagnostic.goto_prev, { desc = "Diagnóstico anterior" })
map("n", "}d", vim.diagnostic.goto_next, { desc = "Siguiente diagnóstico" })

-- Mantener cursor centrado al hacer scroll
map("n", "<C-d>", "<C-d>zz", { desc = "Scroll down and center" })
map("n", "<C-u>", "<C-u>zz", { desc = "Scroll up and center" })

-- Mantener cursor centrado en búsquedas
map("n", "n", "nzzzv", { desc = "Next search result (centered)" })
map("n", "N", "Nzzzv", { desc = "Prev search result (centered)" })

-- Permite mover bloques de contenido, tanto en modo visual como normal, de forma más visual
map("n", "<A-k>", ":m .-2<CR>==", { desc = "Move line up" })
map("n", "<A-j>", ":m .+1<CR>==", { desc = "Move line down" })
map("v", "<A-j>", ":m '>+1<CR>gv=gv", { desc = "Move selection down" })
map("v", "<A-k>", ":m '<-2<CR>gv=gv", { desc = "Move selection up" })

-- Mejora el identado en modo visual
map("v", "<", "<gv", { desc = "Indent left and reselect" })
map("v", ">", ">gv", { desc = "Indent right and reselect" })

-- Guardado más rápido
map({ "i", "x", "n", "s" }, "<C-s>", "<cmd>w<CR><Esc>", { desc = "Save File" })

-- Mejora el comportamiento de J
map("n", "J", "mzJ`z", { desc = "Join lines and keep cursor position" })

--- Borra las highlights
map("n", "<leader>uh", ":nohlsearch<CR>", { desc = "Clear search highlights" })

-- Quick access
map("n", "<leader><space>", function() Snacks.picker.smart() end, { desc = "Smart Find Files" })
map("n", "<leader>,", function() Snacks.picker.buffers() end, { desc = "Buffers" })

-- Explorer
map("n", "<leader>e", function() Snacks.explorer() end, { desc = "Explorer" })

-- Find
map("n", "<leader>fb", function() Snacks.picker.buffers() end, { desc = "Buffers" })
map("n", "<leader>fr", function() Snacks.picker.recent() end, { desc = "Recent Files" })
map("n", "<leader>ff", function() Snacks.picker.files() end, { desc = "Find Files (all)" })
map("n", "<leader>fg", function() Snacks.picker.git_files() end, { desc = "Find Git Files" })

-- Search
map("n", "<leader>sg", function() Snacks.picker.grep() end, { desc = "Grep (live)" })
map({ "n", "x" }, "<leader>sw", function() Snacks.picker.grep_word() end, { desc = "Grep Word/Selection" })
map("n", "<leader>sW", function() Snacks.picker.grep_word({ word = vim.fn.expand("<cword>") }) end, { desc = "Grep Word Under Cursor" })
map("n", "<leader>sh", function() Snacks.picker.help() end, { desc = "Help Pages" })
map("n", "<leader>sk", function() Snacks.picker.keymaps() end, { desc = "Keymaps" })
map("n", "<leader>sc", function() Snacks.picker.command_history() end, { desc = "Command History" })
map("n", "<leader>sd", function() Snacks.picker.diagnostics() end, { desc = "Diagnostics" })
map("n", "<leader>sy", function() Snacks.picker.cliphist() end, { desc = "Cliphist" })

-- Code Action
map("n", "<leader>ca", vim.lsp.buf.code_action, { desc = "Code Action" })

-- LSP
map("n", "<leader>o", function() Snacks.picker.lsp_symbols() end, { desc = "Outline/Symbols" })
map("n", "gd", function() Snacks.picker.lsp_definitions() end, { desc = "Goto Definition" })
map("n", "gr", function() Snacks.picker.lsp_references() end, { desc = "Find References" })
map("n", "gI", function() Snacks.picker.lsp_implementations() end, { desc = "Goto Implementation" })
map("n", "gy", function() Snacks.picker.lsp_type_definitions() end, { desc = "Goto Type Definition" })

-- Notifications
map("n", "<leader>nh", ":NoiceHistory<CR>", { desc = "Message History" })
map("n", "<leader>nd", ":NoiceDismiss<CR>", { desc = "Dismiss Messages" })
map("n", "<leader>nl", ":NoiceLast<CR>", { desc = "Show last message" })

-- Terminal 
map("n", "<leader>tt", function() Snacks.terminal() end, { desc = "Terminal" })

-- Buffer
map("n", "<leader>bd", function() Snacks.bufdelete() end, { desc = "Delete Buffer" })
map("n", "}b", '<cmd>bnext<CR>', { desc = "Next Buffer" })
map("n", "{b", '<cmd>bprevious<CR>', { desc = "Prev Buffer" })
map("n", "<S-l>", '<cmd>bnext<CR>', { desc = "Next Buffer" })
map("n", "<S-h>", '<cmd>bprevious<CR>', { desc = "Prev Buffer" })

-- Words navigation
map("n", "}}", function() Snacks.words.jump(vim.v.count1) end, { desc = "Next Reference" })
map("n", "{{", function() Snacks.words.jump(-vim.v.count1) end, { desc = "Prev Reference" })

-- Extras
map("n", "<leader>z", function() Snacks.zen() end, { desc = "Zen Mode" })
map("n", "<leader>.", function() Snacks.scratch() end, { desc = "Scratch Buffer" })
map("n", "<leader>S", function() Snacks.scratch.select() end, { desc = "Select Scratch" })
map("n", "<leader>cR", function() Snacks.rename.rename_file() end, { desc = "Rename File" })

-- GIT
map("n", "<leader>gb", function() Snacks.gitbrowse() end, { desc = "Git Browse" })
map("n", "<leader>gl", function() Snacks.picker.git_log() end, { desc = "Git Log" })
map("n", "<leader>gs", function() Snacks.picker.git_status() end, { desc = "Git Status" })
map("n", "<leader>gL", function() Snacks.picker.git_log_file() end, { desc = "Git Log (file)" })

lua.init

Ahora unimos todo en nuestro lua.init

-- ~/.config/nvim/init.lua

-- Establece el líder de mapeo de teclas (<leader>) (Usualmente la barra espaciadora)
vim.g.mapleader = " "
vim.g.maplocalleader = "\\"

-- Habilita los colores verdaderos en el terminal, esencial para los temas modernos
vim.opt.termguicolors = true

require("config.options")       -- Carga las opciones generales del editor
require("config.diagnostics")   -- Carga las opciones de diagnóstico del LSP
require("config.folding")       -- Carga la configuración del folding 
require("config.keymaps")       -- Carga nuestros keymaps

-- Bootstrap lazy.nvim
-- Este bloque de código asegura que lazy.nvim esté instalado.
-- Si no lo está, lo clona desde GitHub.
local lazypath = vim.fn.stdpath("data").. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then
  vim.fn.system({
    "git",
    "clone",
    "--filter=blob:none",
    "https://github.com/folke/lazy.nvim.git",
    "--branch=stable", -- última versión estable
    lazypath,
  })
end
vim.opt.rtp:prepend(lazypath)

-- Configura lazy.nvim para cargar tus plugins desde `lua/plugins/`
require("lazy").setup({
  spec = {
    { import = "plugins" },
  },
  -- Opciones adicionales para lazy.nvim
  checker = {
    enabled = true,     -- Habilita la verificación automática de actualizaciones de plugins
    frequency = 86400,  -- Frecuencia de verificación en segundos (cada 24 horas)
  },
})

Algo que también vale mucho la pena reiniciar nvim y ver los primeros cambios, aunque veremos en consola un mensaje como el siguiente:

Se ha detectado un error al procesar /home/alortiz/.config/nvim/init.lua:
No specs found for module "plugins"

Causado porque aún no hay plugins que cargar, pero que lo arreglaremos en la siguiente entrada