wandres.dev
NIVEL DIOS: EL EDITOR · síntesis y contribuir

Leer el código fuente: el repositorio como documentación definitiva

Cómo está organizado el árbol de Neovim, qué contiene cada directorio y en qué orden conviene leerlo. Seguir una llamada de Lua hasta la función en C que la implementa, entender la generación de la API, y convertir la lectura de código ajeno en un método de aprendizaje reproducible.

⏱ 24 min

Llega un momento, y tú ya has llegado a él, en que la documentación deja de ser suficiente. No porque esté mal escrita —la de Neovim es excelente— sino porque describe el contrato y tú empiezas a tener preguntas sobre la implementación: por qué esta función es segura en contexto rápido y aquella no, qué ocurre exactamente con las marcas extendidas cuando se borra un rango, cuánto cuesta realmente esa llamada que haces en cada pulsación. Todas esas preguntas tienen respuesta y la respuesta está escrita, en un repositorio público, en lenguajes que conoces o casi. Leer el código fuente del editor no es una hazaña reservada a los mantenedores: es la siguiente herramienta de trabajo, y como toda herramienta se aprende con un método.

🎯 Al terminar esta lección sabrás
  • Situar los directorios principales del repositorio y saber qué clase de respuestas ofrece cada uno.
  • Seguir una llamada desde vim.api hasta la función en C que la implementa, pasando por el código generado.
  • Usar el historial de Git y un servidor de lenguaje para navegar código ajeno con eficacia.
  • Leer las pruebas funcionales como especificación ejecutable del comportamiento del editor.

El mapa del repositorio

El árbol de fuentes se entiende en cinco bloques, y saber cuál abrir ahorra horas.

src/nvim/ contiene el núcleo en C. Dentro, la separación importa más que los nombres de fichero: event/ envuelve libuv y define el bucle y sus colas; os/ aísla las diferencias de sistema operativo; api/ implementa la superficie pública; msgpack_rpc/ traduce mensajes a llamadas; lua/ incrusta el intérprete y convierte tipos entre ambos mundos; tui/ es la interfaz de terminal, que aquí es un cliente más; y en la raíz viven los ficheros heredados de Vim con los nombres de siempre, donde normal.c es el autómata del modo normal, memline.c el almacenamiento paginado del buffer, undo.c el árbol de deshacer y drawscreen.c la decisión de qué hay que repintar.

runtime/ es la parte escrita en Lua y Vimscript que se distribuye con el editor. runtime/lua/vim/ es probablemente el mejor material de lectura de todo el proyecto: lsp/, treesitter/, diagnostic.lua, iter.lua, shared.lua. Es Lua idiomático, escrito por gente que conoce la API entera, y usa exactamente las mismas funciones que tú. runtime/doc/ guarda la documentación, y conviene saber que buena parte de la referencia de vim.* se genera desde los comentarios del propio código, de modo que corregir un texto de ayuda suele significar editar un comentario en Lua.

Dentro de runtime/ conviene fijarse en un patrón que se repite y que es una lección de diseño en sí mismo: los módulos grandes están partidos en un fichero público que define la interfaz y varios privados, marcados con un guion bajo inicial, que contienen la implementación. Esa convención —pública sin prefijo, interna con él— es una respuesta barata al problema de tener una superficie estable sin renunciar a reorganizar por dentro, y es directamente copiable a tus propios plugins.

test/ es la red de seguridad y, por accidente feliz, la mejor especificación disponible. test/functional/ arranca instancias reales de Neovim y las conduce por el protocolo, comprobando el estado y hasta la pantalla resultante; test/unit/ llama a funciones en C directamente desde LuaJIT; y las pruebas heredadas de Vim siguen ahí, custodiando compatibilidad.

Los dos bloques restantes son src/nvim/generators/, donde viven los guiones que producen código durante la compilación, y la infraestructura de proyecto —el sistema de construcción, la integración continua y los documentos de contribución y mantenimiento— que será el material del capítulo siguiente.

Hay un principio de orientación que ahorra mucho tiempo y que se deduce del mapa: el nombre del fichero suele decirte a qué época pertenece. Los directorios en minúscula con nombres de subsistema —event, api, msgpack_rpc, lua— son código nuevo, escrito con criterios modernos, con funciones cortas y responsabilidades declaradas. Los ficheros sueltos en la raíz con nombres de tres o cuatro letras son herencia de Vim: funciones largas, variables globales, banderas de estado compartidas y comentarios que a veces datan de los años noventa. No es una crítica, es una advertencia de calibrado: si tu primera incursión en el núcleo es una función de mil líneas del código heredado, concluirás erróneamente que el proyecto es ilegible.

💡
Un orden de lectura que funciona

Empieza por runtime/lua/vim/, porque es Lua y ya sabes qué debería hacer. Sigue por src/nvim/api/, que es C sencillo y sobre todo fontanería: validar argumentos, llamar al núcleo, rellenar el error. Deja para el final los ficheros heredados de la raíz, que arrastran treinta años de decisiones y se leen mucho mejor cuando ya conoces el vocabulario del proyecto.

Seguir una función hasta el fondo

El ejercicio fundacional consiste en tomar una llamada que hagas a diario y perseguirla hasta el final. Usemos la que escribe líneas en un buffer.

En Lua, vim.api.nvim_buf_set_lines es una función en una tabla. Esa tabla no está escrita a mano: se construye al compilar, desde las firmas en C. El primer descubrimiento del recorrido es, por tanto, que buscar el nombre de la función en el código Lua no da nada, y ese callejón sin salida es informativo: significa que estás ante código generado.

# Localizar la implementacion real y su generacion
rg -n "nvim_buf_set_lines" src/nvim/api/buffer.c
rg -n "nvim_buf_set_lines" build/    # el despachador y los enlaces generados
ls src/nvim/generators/              # gen_api_dispatch.lua y companeros

La implementación en C tiene una forma reconocible que se repite en toda la capa. Recibe los argumentos ya deserializados, un puntero a un objeto de error y, en las funciones que manejan datos grandes, un asignador de memoria por arena para no fragmentar el montón. Valida: que el buffer exista, que el rango sea coherente, que el texto no contenga caracteres nulos. Después llama a las funciones del núcleo que hacen el trabajo de verdad —insertar y sustituir líneas en el almacenamiento paginado, registrar el cambio en el árbol de deshacer, desplazar las marcas extendidas, marcar la región como sucia para el redibujado— y termina notificando el cambio a quien esté escuchando.

// La forma canonica de una funcion de la capa de API, reducida a su esqueleto
void nvim_buf_set_lines(uint64_t channel_id, Buffer buffer,
                        Integer start, Integer end, Boolean strict_indexing,
                        ArrayOf(String) replacement, Arena *arena, Error *err)
{
  buf_T *buf = find_buffer_by_handle(buffer, err);   // 1. resolver el asa
  if (!buf) { return; }                              // 2. salir por el objeto de error
  // 3. normalizar indices y validar el rango contra el tamanio real
  // 4. mutar el almacenamiento: ml_replace, ml_append, ml_delete
  // 5. propagar: extmark_splice, changed_lines, deleted_lines
}

Ese descenso enseña tres cosas que ninguna documentación dice con esa claridad. La primera es dónde está el coste: verás que ciertas llamadas disparan una invalidación amplia y otras no, y eso explica diferencias de rendimiento que desde fuera parecían caprichosas. La segunda es qué es atómico: verás en qué punto exacto se emiten los eventos, y por tanto qué observará un autocomando que se ejecute a mitad de una operación compuesta. La tercera es por qué existen las restricciones: la anotación que marca una función como utilizable en contexto rápido no es una etiqueta arbitraria, sino la afirmación de que su implementación no toca estructuras que puedan estar a medio actualizar.

flowchart TB
L[Llamada en Lua a vim.api] --> G[Enlaces generados al compilar]
G --> D[Despachador generado desde las firmas]
D --> A[Funcion en src nvim api buffer punto c]
A --> V[Validacion y objeto de error]
V --> M[Nucleo: almacenamiento del buffer]
M --> U[Registro en el arbol de deshacer]
M --> X[Desplazamiento de marcas extendidas]
M --> R[Region marcada como sucia]
R --> P[Eventos de redibujado a los clientes]
style D fill:#cba6f7,color:#11111b
style M fill:#89b4fa,color:#11111b
style P fill:#a6e3a1,color:#11111b

Herramientas para leer código ajeno

Leer un proyecto grande sin instrumentos es leer con una linterna en una biblioteca. Cuatro técnicas cubren casi todo.

La primera es preguntar por el historial en vez de por el estado. Buscar cuándo apareció una cadena o desapareció una condición suele contestar el porqué mucho mejor que mirar el código actual, porque el mensaje de commit y la discusión asociada contienen el razonamiento que el código no puede expresar.

git log -S "nvim_buf_set_extmark" --oneline -- src/nvim/api/extmark.c
git log --follow -p runtime/lua/vim/diagnostic.lua | head -200
git blame -L 120,160 src/nvim/api/buffer.c

La segunda es compilar con base de datos de compilación y usar un servidor de lenguaje sobre el propio Neovim. Con eso, saltar a la definición, listar referencias y ver la jerarquía de llamadas funciona igual que en tu proyecto, y el editor que estás estudiando se convierte en la herramienta con la que lo estudias. Es el momento en que todo el nivel anterior sobre servidores de lenguaje deja de ser teoría.

make CMAKE_BUILD_TYPE=Debug      # genera build/compile_commands.json
ln -s build/compile_commands.json .
nvim src/nvim/api/buffer.c       # con clangd activo: gd, grr, K funcionan

La tercera es leer las pruebas antes que la implementación. Una prueba funcional describe una situación inicial, una acción y un resultado observable; leídas en bloque, las pruebas de un módulo son la lista completa de casos que sus autores consideran contractuales, incluidos los raros que jamás habrías supuesto. Cuando algo del editor te sorprenda, buscar el nombre del comportamiento en test/functional/ suele dar antes con la respuesta que buscar en el código. Y hay un corolario práctico: si un comportamiento no aparece en ninguna prueba, no es un contrato, es un accidente de la implementación actual. Apoyarse en él es apostar a que nadie refactorizará esa zona.

La cuarta es acotar el objetivo antes de empezar. La lectura sin pregunta se agota en veinte minutos; la lectura con una pregunta concreta —qué ocurre con el cursor cuando se borra la línea donde está, cuántas veces se llama a esta función durante un desplazamiento de página— termina siempre con una respuesta y con un mapa mental que se queda.

A esas cuatro conviene añadir un hábito que no es una herramienta sino una postura: instrumentar en vivo lo que estás leyendo. No hace falta recompilar para comprobar una hipótesis sobre el comportamiento del núcleo; casi todo lo interesante se puede observar desde Lua colgando una devolución de llamada, contando invocaciones o cronometrando. La combinación de leer una función y verla ejecutarse con tus propios datos es mucho más rápida que cualquiera de las dos por separado, porque cada lectura genera una predicción y cada medición la confirma o la desmiente en segundos.

-- Instrumentar en vivo lo que estas leyendo, sin recompilar
local n = 0
vim.api.nvim_buf_attach(0, false, {
  on_bytes = function() n = n + 1 end,
})
vim.keymap.set("n", "<leader>c", function() vim.notify("cambios: " .. n) end)
🔱

Lo generado no se lee

Si no encuentras una función, sospecha del generador antes que de tu búsqueda.

🧭

El historial explica

git log -S encuentra el commit que introdujo una idea, y con él su justificación.

📐

Las pruebas especifican

Cada caso de test/functional/ es una afirmación sobre el comportamiento que puedes ejecutar.

⚠️
El riesgo de leer sin medir

Leer código produce una sensación de comprensión que a veces es falsa. Una función puede parecer costosa y ejecutarse una vez por sesión, o parecer trivial y llamarse cien mil veces por segundo. La lectura te dice qué hace el código; solo la medición te dice qué importa. Cada conclusión sobre rendimiento que saques de una lectura es una hipótesis hasta que la cronometres.

Leer código ajeno es la única forma de adquirir criterio que no se puede sustituir por documentación

Hay una asimetría incómoda en cómo se aprende ingeniería de software: escribimos código todos los días y leemos código profesional casi nunca. La consecuencia es que la mayoría de nosotros aprende a programar como quien aprendiera a escribir habiendo leído solo manuales de gramática y ningún libro. La documentación te dice qué hace una función, y eso basta para usarla; el código te dice por qué la hicieron así, y eso es lo único que produce criterio. Cuando abres src/nvim/api/ y ves que cada función valida sus argumentos de forma paranoica antes de tocar nada, no estás aprendiendo una llamada: estás viendo cómo se diseña una frontera que va a ser atravesada por programas hostiles, mal escritos o simplemente distintos de los que imaginaste. Cuando descubres que la API se genera desde una única fuente, no estás aprendiendo un detalle de compilación: estás viendo una respuesta concreta al problema de mantener tres representaciones sincronizadas para siempre, y esa respuesta te servirá en tu propio trabajo la próxima vez que te veas copiando una firma en dos sitios. Cuando lees las pruebas funcionales y compruebas que conducen editores reales en vez de fingirlos, estás recibiendo una posición argumentada sobre qué merece la pena verificar. Nada de eso está escrito en ningún manual, y no podría estarlo, porque no son hechos sino juicios encarnados en decisiones. Por eso el paso que da este capítulo es más importante de lo que su tema aparenta: el día en que dejas de tratar el editor como una caja cerrada y empiezas a leer cómo está hecho, adquieres una capacidad que ya no depende de este proyecto ni de este lenguaje. Ante cualquier sistema del que dependas —una biblioteca, un framework, el compilador, el sistema operativo— tendrás la opción de abrirlo y averiguar. La mayoría de los programadores no ejerce nunca esa opción, y toda la distancia entre saber usar herramientas y entender sistemas cabe justo ahí.

⚔️ Un descenso completo
  1. Clona el repositorio, compila en modo depuración y abre el proyecto con clangd activo comprobando que el salto a definición funciona.
  2. Elige nvim_buf_set_extmark y recórrela desde vim.api hasta la implementación en C, anotando en qué punto se valida, en qué punto muta el estado y en qué punto se notifica el cambio.
  3. Usa git log -S para encontrar el commit que introdujo esa función y lee su descripción y su discusión asociada.
  4. Localiza en test/functional/ las pruebas de las marcas extendidas y enumera tres comportamientos contractuales que no conocías.
  5. Formula una pregunta propia sobre el editor que la documentación no responda, contéstala leyendo el código, y escribe la respuesta en cinco líneas citando fichero y función.