wandres.dev
⚙️ 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.