SGCG

…esto no es un subtítulo…

Ir a: contenido categorías calendario archivo suscripción

Volver arriba

Emacs Lisp en casos prácticos (1): modificando un modo con ad-hook y advice-add

2015-06-30

GNU Emacs es un editor de textos potentísimo. Por debajo, es un intérprete de Emacs Lisp, un lenguaje de programación de la familia de Lisp. En esta serie de artículos vamos a mostrar algunas formas de adaptar Emacs a nuestras necesidades con algunos casos prácticos.

Hoy veremos dos formas de modificar una funcionalidad existente de Emacs sin cambiar su código fuente: add-hook y advice-function. La primera forma es la recomendable siempre que es posible: los programadores añaden puntos donde hacer modificaciones (hooking) y esperan que los usuarios los usen. La segunda forma es un último recurso que apenas está un paso antes de llegar a modificar el código fuente directamente: consiste en crear funciones nuevas para preprocesar o postprocesar el trabajo de cualquier función existente aunque el programador original no hubiera considerado tal opción, lo que puede tener impactos no anticipados y difíciles de trazar en otro código.

El ejemplo de hoy funciona en GNU Emacs 24. Hay versiones anteriores que proporcionan una característica similar a la discutida y modificada en el presente artículo, pero implementada de una forma diferente.

El problema

Emacs tiene una funcionalidad muy útil que consiste en ejecutar otras aplicaciones e interactuar con ellas a través de un búfer como los que se usan normalmente para editar texto y con todas las posibilidades avanzadas de edición que ofrece Emacs. Hay una función interactiva, list-processes, que sirve para mostrar una tabla con los subprocesos que están ejecutándose y que es útil para buscar un proceso concreto y saltar al búfer que tiene asociado. Lo que quizá puede echarse en falta es la opción de gestionar los procesos (pausarlos, reanudarlos, terminarlos, cambiarles la prioridad…). Hoy, para ilustrar dos formas distintas de modificar el comportamiento de una función de Emacs sin cambiar su código fuente, vamos a ofrecer una acción muy concreta: matar un proceso y eliminar el búfer que tiene asociado. Nuestro objetivo es emular el comportamiento de list-buffers (que sirve para mostrar los búferes abiertos):

Plan de acción

Tenemos que hacer lo siguiente:

Al invocar list-processes, se crea un búfer con el modo mayor process-menu-mode, que está derivado a su vez del modo mayor tabulated-list-mode. Si miramos el código fuente de process-menu-mode y sus funciones auxiliares (una gran característica de GNU Emacs es que permite mirar rápidamente el código fuente de cualquier función), vemos hay dos puntos donde podemos atacar. Uno de estos puntos es la activación del modo process-menu-mode (donde es conveniente crear las combinaciones de teclas y modificar el formato de la tabla de búferes) y el otro es la función que se encarga de actualizar el contenido de la tabla de búferes: list-processes--refresh (donde es conveniente añadir las marcas de borrado).

Funciones básicas

Vamos a crear las funciones que necesitamos para marcar, desmarcar y eliminar. La primera, la de marcar, lo único que tiene que hacer es cambiar el contenido de la primera columna de la tabla (que es la que reservamos para la marca) y poner la marca D. Esto lo hacemos con la función tabulated-list-set-col. Por comodidad, terminamos avanzando a la siguiente entrada con forward-line. La función queda como sigue:

(defun process-menu-mark-for-deletion () "Mark the process on this Process List buffer line for deletion." (interactive) (tabulated-list-set-col 0 "D" t) (forward-line))

El desmarcado es igual, pero en vez de poner la marca D, dejamos la casilla vacía:

(defun process-menu-unmark (&optional backup) "Unmark the process on this Process List buffer line." (interactive "P") (tabulated-list-set-col 0 "" t) (forward-line))

Finalmente, hemos de crear la función que borra los procesos (y sus búferes) marcados para ser borrados. Para ello, tenemos que recorrer el contenido de la lista de procesos (la variable tabulated-list-entries). Cada elemento de esta lista es a su vez otra lista con el siguiente contenido:

De cada entrada, nos interesan el proceso y la marca de borrado. Si la marca de borrado está activa (es una D), eliminamos tanto el proceso (con delete-process) como el búfer (con kill-buffer sobre el búfer obtenido mediante process-buffer). La función es así:

(defun process-menu-execute () "Delete the processes marked for deletion." (interactive) (dolist (entry tabulated-list-entries) (let ((process (elt entry 0)) (mark (elt (elt entry 1) 0))) (if (string-equal "D" mark) (delete-process process) (kill-buffer (process-buffer process))))) (revert-buffer))

Modificando process-menu-mode con una función hook

Los modos mayores ofrecen la opción de añadir funciones hook que se ejecutan al entrar en ellos. Se trata de una forma muy sencilla de adaptar la funcionalidad de un modo a nuestras necesidades. Nuestra función hook crea combinaciones de teclas (con local-set-key) y añade una columna (reservada para la marca de borrado) al principio del formato de la tabla (la variable tabulated-list-format):

(defun process-menu-mode--enable-process-management () (local-set-key (kbd "\C-k") 'process-menu-mark-for-deletion) (local-set-key (kbd "k") 'process-menu-mark-for-deletion) (local-set-key (kbd "\C-d") 'process-menu-mark-for-deletion) (local-set-key (kbd "d") 'process-menu-mark-for-deletion) (local-set-key (kbd "u") 'process-menu-unmark) (local-set-key (kbd "x") 'process-menu-execute) (setq tabulated-list-format (vconcat [("" 1 nil)] tabulated-list-format)))

Colgamos la función hook con add-hook sobre process-menu-mode-hook:

(add-hook 'process-menu-mode-hook #'process-menu-mode--enable-process-management)

Modificando list-processes--refresh con una función advice

Otra forma de modificar la funcionalidad de una función existente es con funciones advice. No es el mecanismo más limpio y puede tener consecuencias indeseadas, ya que el código sobre el que se trabaja no tiene por qué haber sido pensado para que el usuario modificara su semántica al vuelo. A pesar de ello, puede ser una opción razonable a veces. Para ilustrar cómo funciona, elegimos esta vía de acción para retocar list-processes--refresh y mostrar las marcas de borrado.

En primer lugar, definimos una función para modificar la tabla de procesos (que está en la variable tabulated-list-entries). Igual que nuestra función process-menu-execute, esta nueva función recorre los elementos de la lista, pero en vez de hacer comprobaciones, añade una celda en blanco (en la que podría haber marca de borrado) al principio de cada vector de celdas a mostrar:

(defun list-processes--add-mark-column () (dolist (entry tabulated-list-entries) (setf (cadr entry) (vconcat [""] (cadr entry)))))

Con la función ya preparada, envolvemos list-processes--refresh con ayuda de advice-add. Podemos elegir cómo ejecutar nuestra función modificadora; esta vez queremos que se ejecute tras la función modificada, así que indicamos la opción :after. La documentación explica qué otras opciones hay. El código es así:

(advice-add 'list-processes--refresh :after #'list-processes--add-mark-column)

Todo el código junto

El código está disponible a través del siguiente enlace: process-menu-modifications.el


Categorías: Informática

Permalink: http://sgcg.es/articulos/2015/06/30/emacs-lisp-en-casos-practicos-1-modificando-un-modo-con-ad-hook-y-advice-add/