Notxor tiene un blog

Defenestrando la vida


Creación de un modo mayor para emacs

Hacía que no me ponía a escribir en el blog, y no es por falta de ganas o por abandono, sino que he estado a otras cosas, que han acabado relacionándose también con el tema de la serie.

Resulta que hemos empezado otro vaporware juego, en forma de aventura conversacional y con la intención de probar otra herramienta para crearlas para web que de momento no había utilizado nunca: ngPAWS. Después de iniciar el proyecto con la versión estable (0.9), nos dimos cuenta que en el repositorio de github había algunos de los errores, que nos estábamos encontrando, corregidos. Así pues, descargamos la versión del repositorio y compilamos. En el paquete estable, en binario, se distribuye un IDE para manejar el fichero de base de datos en modo texto que luego se compilará en la aventura, pero las piezas fundamentales para hacerlo son otros dos binarios:

Como decía, la versión estable cuenta con un IDE que facilita la modificación de la base de datos accediendo de manera directa a los distintos apartados y con unos «chismáticos» de los habituales en todo IDE para guardar, compilar y lanzar el juego. Sin embargo, al descargar los fuentes y compilar, vimos que el IDE es independiente, de hecho está programado en otro lenguaje: Pascal, mientras que el compilador lo está en C. No tengo compilador de Pascal, Lazarus ocuparía más sitio del que puedo permitirme en mi viejo disco duro. Algo habrá que hacer.

Resumiendo, no tenemos IDE, pero tenemos Emacs. Total, el fichero de base de datos es texto plano. Vale, pero queremos que nos facilite la vida: que salte a las secciones correspondientes, como el IDE, que resalte la sintaxis y que compile. ¿Podemos hacer que Emacs haga todas esas cosas para nosotros? Pues sí, pero necesitamos programar un modo mayor de Emacs.

Un modo mayor es algo más complicado que uno menor, que ya lo habíamos visto en anteriores entregas. Por eso, la forma recomendada en el manual de elisp ese hacerlo derivando el nuevo modo de otro modo primigenio. Quizá con el código se vea más claro:

;; Definición del modo 
;;;###autoload
(define-derived-mode paws-mode
  prog-mode "Paws"
  "Toggle Paws mode.
Interactivamente sin argumento, este comando des/activa el modo."
  ;; Colorear sintaxis
  (setq font-lock-defaults '((paws-font-lock-keywords))))

Antes de explicar nada quiero llamar la atención sobre el comentario «automágico» ;;;###autoload. Autoload es un mecanismo por el cual Emacs conoce la existencia de, en este caso, paws-mode y lo puede cargar la primera vez que es llamado el modo, aún sin tenerlo cargado previamente en memoria. Es un poco de azúcar sintáctico para la forma (autoload ...). Si tenéis curiosidad en el manual viene mejor explicado de lo que puedo hacer en cuatro líneas.

Pero vamos a lo que estamos: en este caso, el paws-mode se ha derivado del prog-mode, un modo básico que proporciona bastante comportamiento esperado por los programadores. Además, si utilizas algunos hooks, que yo utilizo en mi init.el, como por ejemplo:

(add-hook 'prog-mode-hook 'display-line-numbers-mode)
(add-hook 'prog-mode-hook 'auto-complete-mode)

para activar el autocompletado y los números de línea, al abrir cualquier archivo de cualquier lenguaje de programación, también funcionarán en el nuevo modo.

Se pueden derivar otros tipos de modo, pues hay varios modos básicos como, por ejemplo, text-mode, para distintos estilos de texto: de él se derivan modos para html o LaTeX. También el modo special-mode que está, principalmente, pensado para buffers generados por emacs más que para contener un fichero. Incluso hay otro modo tabulated-list-mode para contener información en forma de tablas, como pueden ser, por ejemplo, la lista de paquetes o varias de las listas que genera magit. Pero no vamos a entrar más allá de informar que existen y están ahí para ayudar.

Coloreado de sintaxis

En nuestro caso, como es modo sencillo lo derivamos como he dicho antes de prog-mode y en su definición lo único que necesitamos es activar el coloreado de sintaxis. Y para eso asignamos a font-lock-defaults una variable que hemos creado con los distintos tipos de palabras especiales del lenguaje. Esa variable paws-font-lock-keywords la definimos como sigue:

(setq paws-font-lock-keywords
      (let* (
	    ;; definir algunas categorías de keywords
	     (x-keywords '("define" "DONE" "GET" "DROP" "WEAR" "REMOVE" "CREATE" "DESTROY" "SWAP"
			   "PLACE" "PUTO" "PUTIN" "TAKEOUT" "DROPALL" "AUTOG" "AUTOD" "AUTOW" "AUTOR"
			   "AUTOP" "AUTOT" "BREAK" "COPYOO" "COPYOF" "COPYFO" "WHATO" "WEIGH" "SET" "BSET"
			   "OSET" "CLEAR" "OCLEAR" "PLUS" "MINUS" "LET" "ADD" "SUB" "COPYFF" "RANDOM"
			   "MOVE" "GOTO" "WEIGHT" "ABILITY" "MODE" "LINE" "GRAPHIC" "PROMPT" "INPUT"
			   "TIME" "PROTECT" "PRINT" "TURNS" "SC0RE" "CLS" "NEWLINE" "MES" "MESSAGE"
			   "SYSMESS" "PICTURE" "PAPEL" "TINTA" "BORDER" "CHARSET" "SAVEAT"
			   "BACKAT" "PRINTAT" "LISTOBJ" "LISTAT" "INVEN" "DESC" "END" "DONE"
			   "NOTDONE" "OK" "SAVE" "LOAD" "RAMSAVE" "RAMLOAD" "ANYKEY" "PAUSA"
			   "PARSE" "NEWTEXT" "SYNONYM" "BEEP" "PROCESS" "DOALL" "RESET" "EXTERN"))
	    (x-types '("noun" "verb" "adjective" "adverb" "preposition" "pronoun" "conjunction"))
	    (x-constants '("const" "pic" "grf" "msc" "snd" "flg" "obj" "loc"))
	    (x-events '("OREF" "ACTION" "WRITE" "WRITELN" "OBJECT" "ATTR" "LISTCONTENTS"))
	    (x-functions '("AT" "NOTAT" "ATGT" "ATLT" "PRESENT" "ABSENT" "WORN" "NOTWORN" "CARRIED"
			   "NOTCARR" "ISAT" "ISNOTAT" "ZERO" "OZERO" "NOTZERO" "ONOTZERO" "EQ" "NOTEQ"
			   "GT" "LT" "SAME" "NOTSAME" "ADJECT1" "ADVERB" "PREP" "NOUN2" "ADJECT2" "CHANCE"
			   "TIMEOUT" "QUIT" "ISNOTLIGHT" "OBJAT" "WHATOX" "WHATOX2" "BZERO" "BNOTZERO"))

	    ;; generar las cadenas regex para cada categoría de keywords
	    (x-keywords-regexp (regexp-opt x-keywords 'words))
	    (x-types-regexp (regexp-opt x-types 'words))
	    (x-constants-regexp (regexp-opt x-constants 'words))
	    (x-events-regexp (regexp-opt x-events 'words))
	    (x-functions-regexp (regexp-opt x-functions 'words)))

	`(
	  (,x-types-regexp . font-lock-type-face)
	  (,x-constants-regexp . font-lock-constant-face)
	  (,x-events-regexp . font-lock-builtin-face)
	  (,x-functions-regexp . font-lock-function-name-face)
	  (,x-keywords-regexp . font-lock-keyword-face)
	  )))

Aún tenemos que depurar en qué categoría meteremos cada una de las keywords, pero el código como se puede ver es muy fácil de entender y muy básico: se definen una serie de variables locales que representan las diferentes categorías, con let* y se rellenan con las palabras que queremos que se coloreen. Después esas listas se convierten con la función regexp-opt en las expresiones regulares (regexp), que espera el código y se almacenan luego en una lista de pares que asigna a cada uno su fuente correspondiente: (regexp . font).

Vale, la sintaxis de la lista de pares es rara y no la hemos visto hasta ahora en nuestro minicurso de programación con elisp, pero la explico en dos patadas:

  • Lo primero que llama la atención es que utilizamos el signo «`» en lugar del habitual quote «'». Eso indica que dentro de la lista encontraremos elementos que no son constantes, sino que se deben calcular (en nuestro caso sustituir la variable por la cadena que contiene: por su valor).
  • Luego, cada elemento que se tenga que calcular va precedido de un carácter «,».

Con ese formato, por ejemplo, podemos hacer cosas como:

`(una lista de ,(+ 2 3) elementos)

que se convertirá en:

(una lista de 5 elementos)

En nuestro caso, como he dicho antes, se ha utilizado esta sintaxis para sustituir el nombre de cada variable, en cada uno de los elementos por su valor correspondiente, pero sólo del primer término de cada par.

Comentarios

Ya hemos visto cómo hacer que las palabras especiales de paws se coloreen, pero aún no hemos hecho nada con los comentarios. En este caso me volví un poco loco (es el primer modo mayor que hago), y no sabía que colorear los comentarios está separado del coloreado de sintaxis como tal. Se mete en otra lista o tabla de sintaxis. Al final también es sencillo de entender y fácil de hacer. El código es el siguiente:

(setq paws-mode-syntax-table
      (let ((tabla (make-syntax-table)))
	(modify-syntax-entry ?\; "<" tabla)
	(modify-syntax-entry ?\n ">" tabla)
	tabla))

Ese código lo que hace es crear una tabla de sintaxis mediante la forma make-syntax-table y una vez creada se modifican dos elementos con la forma modify-syntax-entry:

  • El carácter ; indica en paws el inicio de los comentarios y se marca con la expresión "<" en la tabla para indicar que inicia el comentario.
  • En paws el comentario se extiende hasta el final de la línea, por tanto cuando se produce un salto de línea, \n, es el final de un comentario, cuando acaba la línea, y se marca con la expresión ">".

Haciéndolo funcionar

Tenemos ya establecido las sintaxis y los comentarios en sus correspondientes tablas, ahora necesitamos hacer que funcione:

(defun paws-mode-variables (&opcional syntax keywords-case-insensitive)
  (when syntax
    (set-syntax-table paws-mode-syntax-table)))

Buscar código en la base de datos

Una de las cosas que facilita esa búsqueda es que todos los apartados están marcados con una cabecera tipo /XXX; por ejemplo, por poner sólo tres: /CTL, /OTX u /OBJ. Al buscar esos textos la marca se moverá dentro del fichero con el siguiente código:

(defun buscar-apartado (apartado)
  "Busca el apartado correspondiente, dado por la cadena de texto."
  (goto-char 1)
  (search-forward apartado))

El sistema es sencillo, se va al primer carácter del búffer con goto-char y luego se hace un search-forward hasta el apartado correspondiente. Luego, llamaremos a esta función con algo tan simple como (buscar-apartado "/OBJ") y ya está desvelado todo el secreto.

Compilar

Para no irme por las ramas, pongo primero el código y luego lo explico. El código es el siguiente:

(defun compilar ()
  "Compila el fichero asociado con el buffer."
  (interactive)
  (message "Compilando: %s" (buffer-file-name))
  (if (eq 0 (shell-command (concat paws-directorio-txtpaws "/txtpaws " (buffer-file-name))))
      (shell-command (concat paws-directorio-ngpc "/ngpc " (file-name-sans-extension buffer-file-name) ".sce"))
    (message "Falló el preprocesador de %s" (buffer-file-name))))

Bueno, tampoco hay mucho que explicar: se lanzan los binarios con shell-command. Primero el preprocesador, comprobando si ha tenido éxito con eq 0. Como decíamos antes, el preprocesador es txtpaws y trabaja directamente con el fichero que editamos: buffer-file-name. Si tiene éxito, luego hay que lanzar el compilador ngpc; sin embargo, hay que hacerlo, no con el fichero que estamos editando, sino con el fichero generado por el paso anterior, que tiene el mismo nombre que el fichero del buffer (file-name-sans-extension) pero con la extensión .sce.

Otros pichorros y chismáticos

Pues eso, para facilitarnos la vida con esto de los modos es conveniente crear combinaciones de teclas y menús que llevarse al cursor del ratón. Esto es muy parecido, por no decir igual, que cuando lo vimos en el ejemplo del modo menor, pero lo repito aquí:

;; Definir las combinaciones de teclas del modo
(defvar paws-mode-map
  (let ((paws-mode-map (make-keymap)))
    (define-key paws-mode-map "\C-c\C-fd" 'ir-define)
    (define-key paws-mode-map "\C-c\C-fc" 'ir-ctl)
    (define-key paws-mode-map "\C-c\C-fv" 'ir-voc)
    (define-key paws-mode-map "\C-c\C-fs" 'ir-stx)
    (define-key paws-mode-map "\C-c\C-fm" 'ir-mtx)
    (define-key paws-mode-map "\C-C\C-ft" 'ir-otx)
    (define-key paws-mode-map "\C-C\C-fl" 'ir-ltx)
    (define-key paws-mode-map "\C-C\C-fn" 'ir-con)
    (define-key paws-mode-map "\C-C\C-fo" 'ir-obj)
    (define-key paws-mode-map "\C-C\C-fp" 'ir-pro)
    (define-key paws-mode-map "\C-C\C-j"  'compilar)
    paws-mode-map)
  "Kaymap para paws-mode.")

(easy-menu-define paws-menu paws-mode-map
  "Menú para el modo paws-mode."
  '("Paws"
    ("Buscar sección"
     ["Ir a defines" ir-define]
     ["Ir a /CTL" ir-ctl]
     ["Ir a /VOC" ir-voc]
     ["Ir a /STX" ir-stx]
     ["Ir a /MTX" ir-mtx]
     ["Ir a /OTX" ir-otx]
     ["Ir a /LTX" ir-ltx]
     ["Ir a /CON" ir-con]
     ["Ir a /OBJ" ir-obj]
     ["Ir a /PRO" ir-pro])
    ["--" nil]
    ["Compilar" compilar]
    ))

Básicamente, consiste en asociar las funciones que hemos definido con los pichorros y chismáticos correspondiente. Creo que el código es muy sencillo de entender y no voy a darle más vueltas.

Instalación

Sólo nos falta instalarlo y ajustarlo a nuestro sitio. Para funcionar, el modo necesita conocer dónde se encuentran los binarios a los que tiene que llamar. Para ello se han creado dos variables:

(defvar paws-directorio-txtpaws nil)
(defvar paws-directorio-ngpc    nil)

El tema es que están vacías, por lo que luego en nuestro init.el debemos establecer esos parámetros para que funcione la compilación. El código de configuración podría quedar de la siguiente manera:

(add-to-list 'load-path "~/proyectos/paws-mode")            ; Sitio donde está «paws-mode»
(require 'paws-mode)
(setq paws-directorio-ngpc "~/proyectos/ngpaws")            ; Sitio donde está el binario «ngpc»
(setq paws-directorio-txtpaws "~/proyectos/ngpaws")         ; Sitio donde está el binario «txtpaws»
(setq auto-mode-alist                                       ; Hacer que la extensión «.txp» enlace el modo paws
      (append '(("\\.txp\\'" . paws-mode)) auto-mode-alist))

Básicamente le decimos a Emacs dónde está instalado el modo y le pedimos que lo cargue. Luego le decimos dónde están los ejecutables y por último añadimos a la lista de modos automáticos la extensión de los ficheros de paws: .txp. Cada vez que abramos un archivo con extensión .txp, la que utiliza paws, se activará el modo paws-mode.

Conclusión

Ha sido una casualidad que necesitara un modo mayor. Recuerdo que dije que era algo más complicado y que no lo tocaríamos de momento, pero ya metido en el tema, tenía que explicarlo. Espero que se haya entendido todo.

Aparte de en el manual de elisp, podéis encontrar también más información sobre programar modos y coloreado de sintaxis en los sitios habituales de Internet.


Comentarios