15 - Ejemplo práctico 6: mini editor con undo/redo

15.1 Objetivo

Crear un editor de texto de consola que permita agregar líneas, borrarlas y deshacer/rehacer operaciones usando pilas. Esta práctica ilustra cómo los editores mantienen bitácoras de cambios.

15.2 Operaciones

Comandos soportados:

  • ADD <texto>: agrega una línea al final.
  • DEL: elimina la última línea.
  • UNDO: revierte la última operación.
  • REDO: reaplica la operación revertida.
  • SHOW: imprime el documento.
  • EXIT: cierra el editor.

15.3 Estructura

  • Una pila para el historial (acciones realizadas).
  • Una pila para redo.
  • Un arreglo dinámico o lista para el documento actual.

15.4 Casos especiales

  • UNDO sobre un estado limpio no hace nada.
  • REDO se limpia cuando llega un comando nuevo (distinto de REDO).
  • Las líneas se almacenan con strdup; recuerda liberar memoria en cada paso.

15.5 Implementación en C

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef enum { ACC_ADD, ACC_DEL } AccionTipo;

typedef struct Nodo {
  AccionTipo tipo;
  char *linea;
  struct Nodo *sig;
} Nodo;

typedef struct {
  Nodo *top;
} PilaAccion;

void pila_init(PilaAccion *p) { p->top = NULL; }
int pila_empty(const PilaAccion *p) { return p->top == NULL; }
void pila_push(PilaAccion *p, AccionTipo tipo, const char *linea) {
  Nodo *n = malloc(sizeof(Nodo));
  n->tipo = tipo;
  n->linea = linea ? strdup(linea) : NULL;
  n->sig = p->top;
  p->top = n;
}
char *pila_pop_linea(PilaAccion *p, AccionTipo *tipo) {
  if (pila_empty(p)) return NULL;
  Nodo *tmp = p->top;
  p->top = tmp->sig;
  if (tipo) *tipo = tmp->tipo;
  char *linea = tmp->linea;
  free(tmp);
  return linea;
}
void pila_clear(PilaAccion *p) {
  AccionTipo dummy;
  while (!pila_empty(p)) free(pila_pop_linea(p, &dummy));
}

typedef struct {
  char *lineas[256];
  int total;
} Documento;

void doc_init(Documento *d) { d->total = 0; }
void doc_add(Documento *d, const char *texto) {
  d->lineas[d->total++] = strdup(texto);
}
char *doc_del(Documento *d) {
  if (d->total == 0) return NULL;
  char *linea = d->lineas[--d->total];
  d->lineas[d->total] = NULL;
  return linea;
}
void doc_show(const Documento *d) {
  puts("----- Documento -----");
  for (int i = 0; i < d->total; ++i) {
    printf("%d: %s\n", i + 1, d->lineas[i]);
  }
}
void doc_clear(Documento *d) {
  while (d->total) free(doc_del(d));
}

int main(void) {
  Documento doc;
  doc_init(&doc);
  PilaAccion undo, redo;
  pila_init(&undo);
  pila_init(&redo);

  char comando[16], buffer[256];
  printf("Mini editor (ADD/D DEL UNDO REDO SHOW EXIT)\n");
  while (1) {
    printf("> ");
    if (scanf("%15s", comando) != 1) break;
    if (strcmp(comando, "EXIT") == 0) break;
    if (strcmp(comando, "ADD") == 0) {
      fgets(buffer, sizeof(buffer), stdin);
      buffer[strcspn(buffer, "\n")] = '\0';
      doc_add(&doc, buffer);
      pila_push(&undo, ACC_ADD, buffer);
      pila_clear(&redo);
    } else if (strcmp(comando, "DEL") == 0) {
      char *linea = doc_del(&doc);
      if (linea) {
        pila_push(&undo, ACC_DEL, linea);
        pila_clear(&redo);
        free(linea);
      }
    } else if (strcmp(comando, "SHOW") == 0) {
      doc_show(&doc);
    } else if (strcmp(comando, "UNDO") == 0) {
      AccionTipo tipo;
      char *linea = pila_pop_linea(&undo, &tipo);
      if (!linea && tipo != ACC_DEL) {
        puts("Nada que deshacer.");
      } else {
        if (tipo == ACC_ADD) {
          char *borrada = doc_del(&doc);
          if (borrada) free(borrada);
          pila_push(&redo, ACC_ADD, linea);
        } else if (tipo == ACC_DEL) {
          doc_add(&doc, linea);
          pila_push(&redo, ACC_DEL, linea);
        }
        free(linea);
      }
    } else if (strcmp(comando, "REDO") == 0) {
      AccionTipo tipo;
      char *linea = pila_pop_linea(&redo, &tipo);
      if (!linea && tipo != ACC_DEL) {
        puts("Nada que rehacer.");
      } else {
        if (tipo == ACC_ADD) {
          doc_add(&doc, linea);
        } else if (tipo == ACC_DEL) {
          char *borrada = doc_del(&doc);
          if (borrada) free(borrada);
        }
        pila_push(&undo, tipo, linea);
        free(linea);
      }
    } else {
      puts("Comando desconocido");
      fgets(buffer, sizeof(buffer), stdin); // limpiar resto
    }
  }

  doc_clear(&doc);
  pila_clear(&undo);
  pila_clear(&redo);
  return 0;
}
Captura del mini editor en consola

15.6 Mejoras sugeridas

  • Persistir el documento para recuperar sesiones.
  • Agregar comandos como INSERT <posición> y REPLACE con su correspondiente historial.
  • Implementar un tope para las pilas de undo/redo, como hacen los editores reales.