⚙️ REFERENCIA · init.lua
Configuración completa
La config modular de la guía, lista para copiar. Cada bloque indica su ruta dentro de~/.config/nvim/. Pasa el ratón por un bloque y pulsa Copiar.
Es la configuración híbrida que recomienda la lección 1.2: vim.pack—el gestor integrado en Neovim 0.12— lleva lo que siempre está cargado, ylazy.nvim solo lo que gana con carga perezosa. El LSP y el completado no llevan plugin: son nativos.
Los archivos específicos de cada lenguaje (lang-c, lang-rust,lang-swift, lang-frontend) y el de dap están en sus lecciones porque incluyen ajustes por proyecto. Aquí tienes el esqueleto completo del IDE.
📄 init.lua
-- Config híbrida: vim.pack lleva el grueso, lazy.nvim lo que
-- gana con carga perezosa. Ver lección 1.2.
-- 1) Leader ANTES de cargar plugins
vim.g.mapleader = " "
vim.g.maplocalleader = " "
require("config.options")
require("config.keymaps")
require("config.autocmds")
-- 2) Lo que siempre está: gestor nativo de 0.12.
-- load = true porque lazy desactiva la carga automática de plugin/.
vim.g.no_plugin_maps = true -- antes de textobjects
vim.pack.add({
{ src = "https://github.com/catppuccin/nvim", name = "catppuccin" },
{ src = "https://github.com/nvim-treesitter/nvim-treesitter", version = "main" },
{ src = "https://github.com/nvim-treesitter/nvim-treesitter-textobjects", version = "main" },
{ src = "https://github.com/nvim-mini/mini.nvim" },
}, { load = true })
require("config.tema")
require("config.treesitter")
require("mini.surround").setup({
-- Prefijo gs: nativamente "dormir N segundos", o sea libre.
-- Deja s y S para flash.nvim (leccion 2.3).
mappings = {
add = "gsa", delete = "gsd", replace = "gsr",
find = "gsf", find_left = "gsF", highlight = "gsh",
},
})
require("mini.pairs").setup({})
require("mini.ai").setup({
-- mini.ai manda en el prefijo i, asi que el objeto "hunk" se registra
-- AQUI y no en gitsigns: si los dos mapean i, el resultado depende de
-- lo rapido que teclees. Ahora ih y ah son un hunk de Git.
custom_textobjects = {
h = function()
local ok, gs = pcall(require, "gitsigns")
if not ok then return nil end
local hunks = gs.get_hunks()
if not hunks then return nil end
local fila = vim.fn.line(".")
for _, h in ipairs(hunks) do
local ini = h.added.start
local fin = ini + math.max(h.added.count, 1) - 1
if fila >= ini and fila <= fin then
return {
from = { line = ini, col = 1 },
to = { line = fin, col = math.max(#vim.fn.getline(fin), 1) },
}
end
end
end,
},
})
-- 3) LSP y completado: nativos, sin plugin
require("config.lsp")
require("config.completado")
-- 4) Lo que gana con carga perezosa
require("config.lazy")📄 lua/config/options.lua
local opt = vim.opt
opt.number = true
opt.relativenumber = true
opt.tabstop = 2
opt.shiftwidth = 2
opt.expandtab = true
opt.smartindent = true
opt.ignorecase = true
opt.smartcase = true
opt.hlsearch = false
opt.incsearch = true
opt.termguicolors = true
opt.signcolumn = "yes"
opt.cursorline = true
opt.scrolloff = 8
opt.wrap = false
opt.splitright = true
opt.splitbelow = true
opt.swapfile = false
opt.undofile = true
opt.clipboard = "unnamedplus"
opt.updatetime = 250
opt.timeoutlen = 400
opt.mouse = "a"
-- Plegado. foldlevelstart = 99 NO es opcional: sin él, con foldmethod
-- expr cada archivo se abre entero plegado. Si te pasa, zR lo abre todo.
opt.foldlevelstart = 99
opt.foldtext = "" -- la línea plegada conserva su resaltado
opt.fillchars:append({ fold = " " })📄 lua/config/keymaps.lua
local map = vim.keymap.set
map("i", "jk", "<Esc>", { desc = "Salir a Normal" })
map("n", "<leader>w", "<cmd>w<cr>", { desc = "Guardar archivo" })
map("n", "<leader>q", "<cmd>q<cr>", { desc = "Cerrar ventana" })
map("n", "<Esc>", "<cmd>nohlsearch<cr>", { desc = "Quitar resaltado" })
map("n", "<C-h>", "<C-w>h", { desc = "Ventana izquierda" })
map("n", "<C-j>", "<C-w>j", { desc = "Ventana abajo" })
map("n", "<C-k>", "<C-w>k", { desc = "Ventana arriba" })
map("n", "<C-l>", "<C-w>l", { desc = "Ventana derecha" })
map("v", "J", ":m '>+1<cr>gv=gv", { desc = "Bajar selección" })
map("v", "K", ":m '<-2<cr>gv=gv", { desc = "Subir selección" })
map("n", "<C-d>", "<C-d>zz")
map("n", "<C-u>", "<C-u>zz")
map("n", "n", "nzzzv")
map("n", "N", "Nzzzv")
map("x", "<leader>p", [["_dP]], { desc = "Pegar sin sobrescribir registro" })📄 lua/config/autocmds.lua
local aug = vim.api.nvim_create_augroup
local au = vim.api.nvim_create_autocmd
au("TextYankPost", {
group = aug("resaltar_yank", { clear = true }),
callback = function() vim.hl.on_yank({ timeout = 200 }) end,
})
au("BufWritePre", {
group = aug("limpiar_espacios", { clear = true }),
pattern = "*",
command = [[%s/\s\+$//e]],
})
au("BufReadPost", {
callback = function()
local mark = vim.api.nvim_buf_get_mark(0, '"')
if mark[1] > 0 and mark[1] <= vim.api.nvim_buf_line_count(0) then
pcall(vim.api.nvim_win_set_cursor, 0, mark)
end
end,
})📄 lua/config/lazy.lua
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", "--branch=stable",
"https://github.com/folke/lazy.nvim.git", lazypath,
})
end
vim.opt.rtp:prepend(lazypath)
require("lazy").setup({
spec = { { import = "plugins" } }, -- sin esto, lua/plugins/ no se lee
install = { colorscheme = { "catppuccin" } },
checker = { enabled = true, notify = false },
ui = { border = "rounded" },
-- Las dos claves de la convivencia con vim.pack:
-- sin ellas, lazy reescribe el runtimepath y tira lo que vim.pack añadió.
performance = {
reset_packpath = false,
rtp = { reset = false },
},
})📄 lua/config/tema.lua
-- Va por vim.pack: un tema nunca se difiere. Si llega tarde,
-- ves el editor gris durante medio segundo.
require("catppuccin").setup({
flavour = "mocha", -- latte, frappe, macchiato, mocha
integrations = {
treesitter = true,
native_lsp = { enabled = true },
which_key = true,
gitsigns = true,
},
})
vim.cmd.colorscheme("catppuccin")📄 lua/config/treesitter.lua
-- Va por vim.pack: la rama main NO admite carga perezosa.
-- Es una reescritura incompatible: no existen nvim-treesitter.configs,
-- ensure_installed, highlight, indent ni incremental_selection.
-- El plugin SOLO instala parsers y aporta queries.
require("nvim-treesitter").install({
"lua", "vim", "vimdoc", "c", "rust", "swift",
"javascript", "typescript", "tsx", "html", "css",
"json", "yaml", "toml", "markdown", "bash",
})
-- El resaltado y el plegado los da Neovim, no el plugin.
-- Sin lista de lenguajes: pregunta si hay parser y actúa.
vim.api.nvim_create_autocmd("FileType", {
callback = function(args)
local lang = vim.treesitter.language.get_lang(vim.bo[args.buf].filetype)
if not lang or not vim.treesitter.language.add(lang) then return end
vim.treesitter.start(args.buf, lang)
-- Plegado por estructura. foldlevelstart = 99 está en options.lua:
-- sin él, todo se abre plegado. za abre y cierra, zR abre todo.
vim.wo[0][0].foldexpr = "v:lua.vim.treesitter.foldexpr()"
vim.wo[0][0].foldmethod = "expr"
end,
})
-- Objetos de texto. El plugin ya no mapea nada: las teclas son cosa tuya.
require("nvim-treesitter-textobjects").setup({
select = { lookahead = true },
move = { set_jumps = true },
})
local sel = require("nvim-treesitter-textobjects.select")
local mov = require("nvim-treesitter-textobjects.move")
local swp = require("nvim-treesitter-textobjects.swap")
for tecla, captura in pairs({
af = "@function.outer", ["if"] = "@function.inner",
ac = "@class.outer", ic = "@class.inner",
aa = "@parameter.outer", ia = "@parameter.inner",
}) do
vim.keymap.set({ "x", "o" }, tecla, function()
sel.select_textobject(captura, "textobjects")
end, { desc = "objeto " .. captura })
end
-- Saltos en ] y [, que es donde Vim los espera.
vim.keymap.set({ "n", "x", "o" }, "]f", function()
mov.goto_next_start("@function.outer", "textobjects")
end, { desc = "siguiente funcion" })
vim.keymap.set({ "n", "x", "o" }, "[f", function()
mov.goto_previous_start("@function.outer", "textobjects")
end, { desc = "funcion anterior" })
-- Acciones en <leader>. Ojo: <leader>ca es la code action del LSP.
vim.keymap.set("n", "<leader>cp", function()
swp.swap_next("@parameter.inner")
end, { desc = "mover parametro adelante" })
vim.keymap.set("n", "<leader>cP", function()
swp.swap_previous("@parameter.inner")
end, { desc = "mover parametro atras" })
-- Selección incremental: NO se configura. Neovim 0.12 la trae de serie en
-- modo Visual: an sube al nodo padre, in baja al hijo, ]n / [n van al nodo
-- siguiente o anterior, ]N / [N extienden la selección.
-- Nunca la mapees a <C-Space>: esa tecla es del autocompletado.📄 lua/plugins/which-key.lua
return {
"folke/which-key.nvim",
event = "VeryLazy",
opts = { preset = "modern" },
}📄 lua/plugins/snacks.lua
return {
"folke/snacks.nvim",
priority = 1000,
lazy = false,
opts = {
picker = { enabled = true },
explorer = { enabled = true },
terminal = { enabled = true },
dashboard = { enabled = true },
indent = { enabled = true },
notifier = { enabled = true },
bigfile = { enabled = true },
},
keys = {
{ "<leader>ff", function() Snacks.picker.files() end, desc = "Buscar archivos" },
{ "<leader>fg", function() Snacks.picker.grep() end, desc = "Grep proyecto" },
{ "<leader>fb", function() Snacks.picker.buffers() end, desc = "Buffers" },
{ "<leader>fr", function() Snacks.picker.recent() end, desc = "Recientes" },
{ "<leader>fs", function() Snacks.picker.lsp_symbols() end, desc = "Símbolos" },
{ "<leader>e", function() Snacks.explorer() end, desc = "Explorador" },
{ "<leader>tt", function() Snacks.terminal() end, desc = "Terminal" },
},
}📄 lsp/lua_ls.lua
-- Neovim 0.12: los servidores son ficheros en la carpeta lsp/ del
-- runtimepath. Ni nvim-lspconfig ni mason son necesarios.
-- El nombre del fichero es el nombre del servidor.
return {
cmd = { "lua-language-server" },
filetypes = { "lua" },
root_markers = { ".luarc.json", ".luarc.jsonc", ".git" },
settings = {
Lua = {
diagnostics = { globals = { "vim", "Snacks" } },
workspace = { checkThirdParty = false },
telemetry = { enable = false },
},
},
}📄 lua/config/lsp.lua
-- Enciende los servidores declarados en lsp/*.lua. Una sola línea.
vim.lsp.enable({
"lua_ls", "clangd", "vtsls", "tailwindcss",
"cssls", "html", "emmet_language_server", "jsonls", "yamlls",
})
-- OJO: en 0.12 estos atajos YA EXISTEN de fábrica. No los remapees.
-- grn renombrar gra code action
-- grr referencias gri implementaciones
-- grt definición de tipo gO símbolos del documento
-- K hover CTRL-] ir a definición
-- CTRL-S (Insert) ayuda de firma
-- ]d / [d diagnósticos CTRL-W d diagnóstico flotante
-- an / in (Visual) rango de selección
-- Solo añade lo que falte, y siempre detrás de <leader>.
vim.api.nvim_create_autocmd("LspAttach", {
group = vim.api.nvim_create_augroup("lsp_attach", { clear = true }),
callback = function(ev)
local cliente = vim.lsp.get_client_by_id(ev.data.client_id)
local map = function(k, fn, desc)
vim.keymap.set("n", k, fn, { buffer = ev.buf, desc = "LSP: " .. desc })
end
map("<leader>cf", function() vim.lsp.buf.format({ async = true }) end, "Formatear")
map("<leader>cD", vim.lsp.buf.declaration, "Declaración")
-- Capacidades condicionadas: solo si el servidor las soporta.
if cliente and cliente:supports_method("textDocument/inlayHint") then
map("<leader>ch", function()
vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled({ bufnr = ev.buf }),
{ bufnr = ev.buf })
end, "Inlay hints")
end
if cliente and cliente:supports_method("textDocument/codeLens") then
vim.lsp.codelens.enable(true, { bufnr = ev.buf }) -- líneas virtuales en 0.12
end
end,
})
vim.diagnostic.config({
virtual_text = { prefix = "●", spacing = 2 },
underline = true,
severity_sort = true,
float = { border = "rounded", source = true },
signs = {
text = {
[vim.diagnostic.severity.ERROR] = " ",
[vim.diagnostic.severity.WARN] = " ",
[vim.diagnostic.severity.HINT] = " ",
[vim.diagnostic.severity.INFO] = " ",
},
},
})📄 lua/config/completado.lua
-- Neovim 0.12 completa solo, sin plugin. Prueba esto antes de instalar nada.
vim.o.autocomplete = true
vim.opt.completeopt = { "menuone", "popup", "noselect", "fuzzy" }
-- Enchufa el LSP al menú automático.
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(ev)
local cliente = vim.lsp.get_client_by_id(ev.data.client_id)
if cliente and cliente:supports_method("textDocument/completion") then
vim.lsp.completion.enable(true, ev.data.client_id, ev.buf, { autotrigger = true })
end
end,
})
-- Ctrl-Espacio invoca el menú a mano. Esta tecla es SOLO del completado:
-- no se la cedas a la selección incremental ni a un plugin de saltos o de IA.
vim.keymap.set("i", "<C-Space>", function() vim.lsp.completion.get() end,
{ desc = "Completado" })📄 lua/plugins/completado.lua (opcional)
-- Solo si quieres más que el nativo: fuzzy fino, varias fuentes
-- ordenadas, snippets. Si lo instalas, apaga vim.o.autocomplete.
return {
"saghen/blink.cmp",
version = "1.*",
dependencies = { "rafamadriz/friendly-snippets" },
event = "InsertEnter",
opts = {
keymap = { preset = "default" },
appearance = { nerd_font_variant = "mono" },
sources = { default = { "lsp", "path", "snippets", "buffer" } },
completion = {
documentation = { auto_show = true, auto_show_delay_ms = 200 },
ghost_text = { enabled = true },
menu = { border = "rounded" },
},
signature = { enabled = true, window = { border = "rounded" } },
fuzzy = { implementation = "prefer_rust_with_warning" },
},
}📄 lua/plugins/edicion.lua
return {
{
"folke/flash.nvim",
event = "VeryLazy",
opts = {},
keys = {
-- NO en s / S: son verbos nativos de Vim y s es el prefijo de
-- operador de mini.surround (sa, sd, sr). Cederlas rompe surround
-- de forma intermitente, según qué plugin cargue último.
{ "s", mode = { "n", "x", "o" }, function() require("flash").jump() end, desc = "Flash: saltar" },
{ "S", mode = { "n", "x", "o" }, function() require("flash").treesitter() end, desc = "Flash: nodo" },
},
},
-- mini.surround, mini.pairs y mini.ai NO van aquí: los lleva vim.pack
-- desde el init.lua. Declararlos en los dos sitios los instala dos veces.
{
"windwp/nvim-ts-autotag",
ft = { "html", "javascriptreact", "typescriptreact", "tsx", "vue", "svelte", "xml" },
opts = {},
},
}📄 lua/plugins/git.lua
return {
{
"lewis6991/gitsigns.nvim",
event = { "BufReadPre", "BufNewFile" },
opts = {
-- Los seis signos: uno vacío hace ese cambio invisible.
signs = {
add = { text = "▎" },
change = { text = "▎" },
delete = { text = "_" },
topdelete = { text = "‾" },
changedelete = { text = "~" },
untracked = { text = "┆" },
},
on_attach = function(buf)
local gs = require("gitsigns")
local map = function(modo, k, fn, desc)
vim.keymap.set(modo, k, fn, { buffer = buf, desc = desc })
end
-- Saltos repetibles con ; y ,
local ok, rep = pcall(require, "nvim-treesitter-textobjects.repeatable_move")
local ir = ok
and rep.make_repeatable_move(function(o) gs.nav_hunk(o.forward and "next" or "prev") end)
or function(o) gs.nav_hunk(o.forward and "next" or "prev") end
map("n", "]h", function() ir({ forward = true }) end, "Siguiente hunk")
map("n", "[h", function() ir({ forward = false }) end, "Hunk anterior")
map("n", "<leader>hs", gs.stage_hunk, "Stage hunk")
map("n", "<leader>hr", gs.reset_hunk, "Reset hunk")
-- En Visual hay que pasar el rango: sin él hace el hunk entero.
map("x", "<leader>hs", function()
gs.stage_hunk({ vim.fn.line("."), vim.fn.line("v") })
end, "Stage de la selección")
map("x", "<leader>hr", function()
gs.reset_hunk({ vim.fn.line("."), vim.fn.line("v") })
end, "Reset de la selección")
map("n", "<leader>hp", gs.preview_hunk, "Preview hunk")
map("n", "<leader>hb", function() gs.blame_line({ full = true }) end, "Blame")
end,
},
},
{
"kdheepak/lazygit.nvim",
cmd = { "LazyGit" },
keys = { { "<leader>gg", "<cmd>LazyGit<cr>", desc = "LazyGit" } },
},
}📄 lua/plugins/format-lint.lua
return {
{
"stevearc/conform.nvim",
event = { "BufWritePre" },
cmd = { "ConformInfo" },
opts = {
formatters_by_ft = {
lua = { "stylua" },
c = { "clang_format" },
rust = { "rustfmt" },
swift = { "swift_format" },
javascript = { "prettierd", "prettier", stop_after_first = true },
typescript = { "prettierd", "prettier", stop_after_first = true },
typescriptreact = { "prettierd", "prettier", stop_after_first = true },
css = { "prettierd", "prettier", stop_after_first = true },
html = { "prettierd", "prettier", stop_after_first = true },
json = { "prettierd", "prettier", stop_after_first = true },
},
format_on_save = { timeout_ms = 1000, lsp_format = "fallback" },
},
},
{
"mfussenegger/nvim-lint",
event = { "BufReadPre", "BufNewFile" },
config = function()
local lint = require("lint")
lint.linters_by_ft = {
javascript = { "eslint_d" },
typescript = { "eslint_d" },
typescriptreact = { "eslint_d" },
sh = { "shellcheck" },
}
vim.api.nvim_create_autocmd({ "BufWritePost", "InsertLeave" }, {
callback = function() lint.try_lint() end,
})
end,
},
}¿Quieres entender cada pieza? Vuelve al Nivel 1 y sigue el camino. ¿Buscas un atajo concreto? Está en el Cheatsheet.