Saltar al contenido

El árbol de deshacer

El historial de Neovim no es una pila: es un árbol, y por eso no se puede perder trabajo deshaciendo. Ramas con g- y g+, persistencia en disco con undofile, viajar en el tiempo con :earlier, qué cuenta exactamente como un cambio, y las dos teclas que casi nadie conoce: U y :undojoin.

En casi todos los editores, deshacer unos pasos y volver a escribir destruye el camino que descartaste: ese texto deja de existir y no hay forma de recuperarlo. Es una limitación tan universal que nadie la percibe como una limitación; se asume que deshacer funciona así. En Neovim no se borra nada. El historial no es una pila, es un árbol, cada desvío queda guardado, y con una opción de una línea ese árbol sobrevive a cerrar el editor y apagar el ordenador. Esta lección cierra el nivel a propósito: es la red que hace razonable todo lo que has aprendido antes.

  • Explicar por qué un historial en árbol no pierde información y una pila sí.
  • Distinguir u y Ctrl-r, que recorren una rama, de g- y g+, que recorren el tiempo.
  • Persistir el árbol entre sesiones con 'undofile', y saber dónde vive.
  • Viajar con :earlier y :later, en pasos, en escrituras o en minutos.
  • Controlar qué cuenta como un cambio, que decide cuánto retrocede cada u.

Una pila pierde información; un árbol no

Imagina esto:

Texto
1. escribes  "hola mundo"2. lo cambias a  "hola Neovim"3. deshaces        → vuelves a "hola mundo"4. escribes  "hola Vim"

En un editor con historial en pila, el paso 4 destruye «hola Neovim». Ese estado deja de existir: tomaste un desvío y el camino anterior se borró.

En Neovim el paso 4 crea una rama:

Las dos ramas son alcanzables — y no te lo cuento: compruébalo ahí arriba. Haz clic en el árbol y pulsa u unas cuantas veces: verás que recorre hola Vim → hola mundo → (vacío) y nunca pasa por «hola Neovim». Ahora pulsa g- y g+: esos sí recorren el tiempo entero, rama incluida. Esa diferencia es la lección.

u y Ctrl-r no recorren el árbol

Aquí está lo que confunde a todo el mundo:

Tecla Se mueve por
u / Ctrl-r la rama actual
g- / g+ el tiempo, saltando entre ramas

u te lleva al estado anterior dentro de la rama en la que estás. Si el estado que buscas está en otra rama, u no llega nunca, por muchas veces que lo pulses.

g- y g+ recorren todos los estados en el orden en que se crearon, sin importar la rama. Y aceptan conteo: 5g- retrocede cinco estados de golpe.

Texto
u        deshacer dentro de esta ramaCtrl-r   rehacer dentro de esta ramag-       el estado ANTERIOR en el tiempo, esté en la rama que estég+       el estado SIGUIENTE en el tiempo

Si «deshacer no encuentra lo que buscas», es esto

Le has dado a u quince veces y ese texto que sabes que escribiste no aparece. No lo has perdido: está en otra rama.

Prueba g- repetidamente. Recorre el tiempo real y acabará pasando por ahí.

Y U, que no es lo que crees

U en mayúscula no es rehacer. Deshace todos los cambios hechos en la última línea que tocaste, de una vez, mientras sigas en ella.

Es una reliquia de vi y sigue siendo útil: acabas de hacer cuatro ediciones a la misma línea y ninguna te convence. U la devuelve a como estaba, sin contar pasos. Y como U es en sí mismo un cambio, otro U lo revierte: alterna.

Que sobreviva a cerrar el editor

Por defecto, el historial se pierde al cerrar el fichero. Una opción lo arregla:

VIM
:set undofile

Con eso, Neovim guarda el árbol completo en disco, en stdpath('state')/undo/, un fichero por cada fichero que editas. Cierras, apagas el ordenador, vuelves mañana, y u sigue deshaciendo lo de ayer. El árbol entero, con sus ramas.

Es de las opciones con mejor relación entre lo que cuesta escribirla y lo que da, y la pondrás en tu configuración en el Nivel 2.

Lo que esto sustituye

Con 'undofile', esos ficheros documento.bak, documento.old y documento_v2_final que la gente deja por las carpetas dejan de tener sentido. El editor guarda todos los estados intermedios y sabe navegarlos.

Y no ocupa lo que parece: se guardan las diferencias, no copias del fichero. Uno de tres mil líneas con doscientos cambios ocupa unos pocos kilobytes.

Dos avisos que casi nadie da

Nadie limpia esos ficheros por ti. Se acumulan indefinidamente, y 'undodir' —que verás recomendado como si fuera la solución— solo dice dónde se guardan, no los borra. Si algún día te molesta, es un borrado de ficheros viejos hecho a mano o desde tu configuración. En la práctica casi nunca llega a importar, pero conviene no creerse que hay una limpieza automática que no existe.

Y el árbol tiene un tope: 'undolevels', que por defecto son mil cambios. Es mucho para un día de trabajo y es poco si te lanzas a una macro de diez mil iteraciones. Si necesitas más, es una opción; si lo pones a un número negativo, desactivas el deshacer por completo, cosa que solo tiene sentido al editar ficheros enormes generados.

Viajar en el tiempo

Deshacer también acepta tiempo real, no número de pasos:

VIM
:earlier 10m      " el fichero como estaba hace 10 minutos:earlier 1h       " hace una hora:later 5m         " 5 minutos hacia adelante:earlier 100f     " 100 ESCRITURAS atrás (f de file):undolist         " todos los estados, con número y marca de tiempo:undo 42          " ir directamente al estado 42

:earlier 1f es el que más se usa y casi nadie conoce: te devuelve el fichero tal y como estaba la última vez que lo guardaste. Has hecho un desastre desde el último :w —o has lanzado una macro de la lección 1.5 que ha salido mal— y no sabes cuántos pasos son. :earlier 1f y listo.

Apúntatelo junto al :cfdo undo | update de la lección 1.6: son las dos formas de deshacer un desastre grande, una dentro de un fichero y otra repartida por muchos.

Qué cuenta como «un cambio»

Esto decide cuánto retrocede cada u, y explica por qué a veces deshace mucho más de lo que esperabas.

Un cambio es todo lo que hiciste entre entrar y salir del modo Insertar. Si escribes tres párrafos sin pulsar Esc, eso es un cambio: un solo u se lo lleva entero.

Por eso conviene salir a Normal de vez en cuando mientras escribes: cada Esc cierra un bloque al que poder volver. Y si quieres puntos intermedios sin salir, ya conoces la tecla desde la lección 0.5: Ctrl-g``u corta el bloque ahí mismo.

De ahí sale un truco que verás en muchas configuraciones y que conviene entender antes de copiarlo:

VIM
" un punto de deshacer en cada espacio y en cada salto de línea:inoremap <Space> <Space><C-g>u:inoremap <CR> <C-g>u<CR>

Con eso, u deshace palabra a palabra en vez de párrafo a párrafo.

Y por qué el orden de esas dos líneas es distinto

Fíjate en que en la primera el <C-g>u va detrás del espacio y en la segunda va delante del <CR>. No es un descuido.

El punto de deshacer se pone donde esté la tecla, así que la primera cierra el bloque después de escribir el espacio: la palabra que acabas de terminar queda dentro del bloque anterior, junto a su espacio. La segunda lo cierra antes del salto de línea: la línea que acabas de terminar queda cerrada y el salto abre bloque nuevo.

Las dos son razonables y hacen cosas ligeramente distintas. Lo que no es razonable es copiarlas sin saber que se comportan de forma diferente, que es lo que ocurre en la mayoría de las configuraciones donde aparecen.

:undojoin: lo contrario, y para qué sirve

A veces quieres exactamente lo opuesto: que el cambio siguiente se fusione con el anterior en vez de ser un bloque nuevo. Eso es :undojoin, y su caso de uso clásico es el formateo al guardar.

Si tu configuración reformatea el fichero al escribirlo, sin :undojoin acabas con dos bloques —tu edición, y el formateo— y necesitas dos u para deshacer lo que para ti fue una sola acción. Con :undojoin delante del formateo, un u deshace las dos cosas, que es lo que esperas.

No lo necesitas hoy. Lo verás en el Nivel 3, cuando montes el formateo automático, y ahora ya sabrás por qué está ahí.

Casi todo lo que llamamos «guardar» es en realidad «destruir lo anterior»

Merece la pena mirar el árbol de deshacer como lo que es: una decisión de no destruir información que era barata de conservar.

Un historial en pila no se diseñó pensando «vamos a perder el trabajo del usuario». Se diseñó cuando la memoria era cara y guardar los desvíos parecía un lujo, y el modelo sobrevivió por inercia mucho después de que el argumento dejara de ser cierto. Hoy conservar esas ramas cuesta unos kilobytes por fichero, y aun así casi ningún editor lo hace, porque la pila es lo que la gente espera y nadie se queja de una capacidad que no sabe que podría tener.

La generalización merece la pena porque es una de las ideas más productivas de los últimos veinte años de software: sustituir la actualización destructiva por la acumulación de estados. Git es exactamente esto para ficheros —un commit no reemplaza al anterior, lo referencia—, y su árbol de ramas es literalmente el mismo objeto que acabas de aprender, con la misma consecuencia práctica: no puedes perder trabajo si no borras nada. Las estructuras de datos persistentes de los lenguajes funcionales, donde «modificar» una lista devuelve una lista nueva que comparte casi toda la anterior. El event sourcing, donde el estado de un sistema no se guarda: se deriva de la lista de todo lo que ha pasado. El registro de escritura anticipada de una base de datos. Las instantáneas de un sistema de ficheros como ZFS o Btrfs. Los CRDT, que conservan las operaciones concurrentes en vez de elegir un ganador.

En todos, la pregunta de diseño es la misma: ¿esta operación destruye algo, y cuánto costaría no destruirlo? Y en todos, la respuesta resultó ser «bastante menos de lo que parecía», con una recompensa que no es la recuperación ante desastres sino algo más sutil: cambia lo que te atreves a probar. Es el mismo argumento de la reversibilidad que viste en la lección 0.3 con la jumplist, un nivel más profundo. Con una pila, deshacer diez pasos para probar otro enfoque es una apuesta: si el nuevo es peor, has perdido el viejo. Con un árbol no es una apuesta, es una visita.

Por eso esta lección cierra el nivel. Todo lo que has aprendido en las siete anteriores —:g sobre trescientas líneas, :cdo sobre cuarenta ficheros, 99@a sin mirar cuántas líneas hay— solo es razonable si equivocarse es barato. El árbol de deshacer es lo que hace que lo sea.

Ver el árbol

Todo lo anterior funciona sin instalar nada, y :undolist te da la lista de estados en texto. Navegar un árbol a ciegas es incómodo, y hay plugins que lo dibujan con sus ramas, sus marcas de tiempo y el diff de cada estado; los verás en el Nivel 3.

Merece la pena instalar uno una vez, mirar el árbol que llevas construyendo, entenderlo visualmente, y después olvidarlo: en el día a día, g- y :earlier 1f cubren prácticamente todo.

u solo recorre la rama actual. g- recorre el tiempo y pasa por todas.

1f es «un guardado atrás». No hay que contar nada.

U deshace todos los cambios de la última línea tocada, de golpe. Y otro U los rehace.

Ir y volver. El árbol no pierde el estado actual por visitar otro.

Corta el bloque de deshacer donde tú digas, sin salir del modo.

Comprueba que no se puede perder nada

  1. Escribe una frase, cámbiala, deshaz, y escribe una tercera versión distinta.
  2. Con u intenta llegar a la segunda versión. No vas a poder: está en otra rama.
  3. Llega con g-. Después mira :undolist y localiza los tres estados con sus horas.
  4. Guarda el fichero, haz un desastre, y recupéralo con :earlier 1f.
  5. Haz cuatro ediciones a una misma línea y revierte las cuatro con U. Pulsa U otra vez.
  6. Activa :set undofile, edita algo, cierra Neovim del todo, vuelve a abrir el fichero y comprueba que u sigue deshaciendo lo de antes. Mira el fichero que se creó en stdpath('state')/undo/.
  7. Prueba los dos mapeos de <C-g>u, escribe un párrafo, y comprueba que ahora u deshace palabra a palabra.
  8. Y el cierre del nivel: lanza sobre un fichero de prueba un :g que toque doscientas líneas, comprueba que u lo revierte de una sola vez, y luego una macro 99@a, y comprueba que esa no. Ahí tienes, en dos comandos, por qué esta lección va la última.