GNU Emacs: Construir un «major mode» paso a paso

emacs

Esta receta explica cómo hacer un _major mode_ para facilitar la edición de un lenguaje de programación, incluyendo resalte de sintaxis e indentado automático.

Introducción

Los emacs-mode son bien conocidos por aquellos que utilizamos emacs como editor para distintas cosas. Es posible que alguna vez te hayas preguntado (o hayas necesitado) cómo crear un mode para emacs; ya sea porque tienes instalado un mode "incompleto" y quieres ampliar su funcionalidad, o bien porque tienes un lenguaje que te has inventado y quieres que tenga soporte en emacs. Sea cual sea tu caso, esta receta te explica como resaltar sintaxis con colores e indentar correctamente tú código.

Vamos a utilizar como lenguaje de programación Emacs-Lisp, que contiene gran soporte para las distintas funciones de Emacs y es "más procedural" que Lisp. El manual de GNU sobre Emacs-Lisp es bastante completo y, para iniciarse, es algo duro. Por este motivo, esta receta explica de forma muy resumida y esquemática cómo hacer un mode para Emacs muy básico.

Especificación del lenguaje de ejemplo

Vamos a suponer que tenemos un lenguaje ya definido y que lo hemos llamado CL (Chorrotronic Language, por aquello del inglés :-)). Un fichero ejemplo de este lenguaje podría ser el siguiente:

BLOQUE inicio {
     ENTERO valor = 3;
     /*Hemos creado e igualado la variable entera valor a 3*/
 
     BULEANO verdad = verdadero;
     SI ( verdad ) {
         IMPRIMIR "Valor de valor", valor;
     }
     EJECUTAR fin;
 
BLOQUE fin {
     //Aquí acaba el programa
     SALIR -1;
}

El ejemplo, además de ser muy tonto, nos indica unas cuantas cosas a tener en cuenta:

  • Las palabras reservadas que tenemos que resaltar son: BLOQUE, ENTERO, IMPRIMIR, SI, BULEANO, EJECUTAR y SALIR.
  • El lenguaje también posee identificadores que deberemos resaltar como tales (por ejemplo, en la declaraciones de BLOQUE y variables).
  • Los valores "verdadero" y "falso" las podemos considerar palabras reservadas, pero las trataremos de forma distinta (sobre todo en el restaldado de sintaxis).
  • Además, tiene comentarios C-Style; pero, como veremos, deberemos tratarlos de distinta forma que el resto de tokens. Las cadenas de caracteres constantes las resalta emacs por defecto, luego no las tendremos en cuenta.
  • El lenguaje CL tiene limitadores de bloque '{}', que deremos tratar para una correcta tabulación.

Creación del fichero y pequeños trucos

Abrimos emacs con el archivo lc-mode.el. Puedes llamarlo como quieras pero suelen decir que los nombres "deben ser descriptivos", así que es mejor seguir los convenios.

Automáticamente, emacs detectará que lo que tratas de escribir es "Emacs Lisp" y nos aparece un botón en la barra de menús muy útil para este modo. Si despliegas el el menú, verás una serie de acciones; entre las que se destacan:

  • Byte-Compile This File: compila el fichero, generando un .elc que es el que Emacs cargará a posteriori.
  • Byte-Compile and Load: además de compilarlo, lo carga en la sesión actual de Emacs. Esta opción es la que puedes utilizar para probar el mode conforme vas construyéndolo.

¡Manos a la obra! Las siguientes secciones explican la estructura básica de un modo de Emacs. No tienes por qué seguirla al pie de la letra, puedes incluír en tu archivo sólo las partes que te sean de utilidad.

Configuración básica

Lo primero que tenemos que incluir es la configuración básica del modo. Veamos el primer fragmento y lo explicamos con detalle:

;Setup básico
(defvar cl-mode-hook nil)

Primero definimos la variable cl-mode-hook que todo mode que se precie definir si quiere ejecutar código propio en el entorno de emacs. En un principio toma el valor 'nil' (false).

(defvar cl-mode-map
  (let ((cl-mode-map (make-keymap)))
    (define-key cl-mode-map "\C-j" 'newline-and-indent)
    cl-mode-map)
  "Keymap for CL major mode")

A continuación definimos el key-map de nuestro major mode. Con la función 'let' declaramos una variable (cl-mode-map) a la que añadimos un mapa de teclado nuevo. Puedes añadir mapas ya predefinidos, pero para el ejemplo es más que suficiente.

Seguidamente, añadimos una combinación (Control+j) que realiza la operación de "nueva línea e indenta". Si quieres añadir más combinaciones personales, este es el lugar adecuado para ello.

(defconst cl-default-tab-width 3)
(add-to-list 'auto-mode-alist '("\\.cl\\'" . cl-mode))

Con las dos líneas anteriores hemos definido 2 cosas muy importantes:

  • La longitud de los tabulados: utilizada en la indentación.
  • La extensión de los archivos: todos los archivos que se carguen con extensión '.cl' harán que el major-mode que estamos definiendo se cargue.

Resaltado de sintaxis

Hasta ahora simplemente hemos configurado nuestro nuevo modo de Emacs. Es hora de entrar de especificar las particularidades de nuestro lenguaje. Vamos a definir el coloreado de sintaxis de la siguiente forma: tendremos 2 niveles de coloreado; el primer nivel sólo coloreará las palabras reservas, mientras que el segundo nivel será mucho más "bonito" y coleará el resto de tokens si procede.

Definimos el primer nivel de coloreado:

(defconst cl-font-lock-keywords-1
  (list
   '("\\<\\(BLOQUE\\|ENTERO\\|IMPRIMIR\\|SI\\|BULEANO\\|EJECUTAR\\)" . font-lock-keyword-face)
   '("\\<\\(SALIR\\)" . font-lock-warning-face))
  "Minimal highlighting expressions for CL mode")

En la constante 'cl-font-lock-keywords-1' definimos el primer nivel de coloreado de sintaxis que requiere una lista sobre los tokens a colorear. Hay muchas formas de pasarle esta lista, pero la más sencilla es "expresion_regular . aspecto".

Hay una forma más elegante de poner la expresión regular (que es utilizando la función 'regexp-opt') pero se desaconseja ponerla en esa línea, debido a que el rendimiento del modo se ve mermado. Hay que tener en cuenta que el coloreado de sintaxis se ejecuta cada vez que se escribe algo.

SALIR es una palabra reservada, pero no le vamos a poner ese estilo. Le vamos a dar un toque más... alarmante :-).

Vamos ahora con el segundo nivel:

(defconst cl-font-lock-keywords-2
  (append cl-font-lock-keywords-1
	  (list
	   '("\\<\\(verdadero\\|falso\\)" . font-lock-constant-face)
	   '("[a-zA-Z0-9_]+ \\<\\([a-zA-Z_][a-zA-Z0-9_]*\\)" 1 font-lock-variable-name-face)))
  "Second level of highlighting expressions for CL mode")

Creamos otra constante con otro nivel más e indicamos a Emacs que el nivel que vamos a describir "concatenado" con el nivel anteriormente descrito. Por lo que lo ya descrito en el nivel se "hereda" en este nivel.

A los valores "BULEANOS" que hemos definido le ponemos aspecto de constante y, por último, pintamos los nombres de variables. Un nombre de variable va seguido, normalmente, por un tipo o, en general, por otro identificador. Por ello la expresión regular tiene una primera parte ([a-zA-Z0-9_]+) que no está encerrada entre el '\\<' que indica qué parte del token hay que pintar. Nótese que ya no se separan los argumentos por el signo '.', sino por '1'. Esto es necesario para que no pinte todo el token del aspecto que se especifica.

Un buen ejercicio sería, intentar pintar el tipo de color verde (font-lock-type-face) y a continuación la variable en su aspecto font-lock-variable-name-face. ;-)

Por último, dar a conocer los niveles de coloreado de sintaxis que hemos definido:

(defvar cl-font-lock-keywords cl-font-lock-keywords-2
  "Default highlighting expressions for CL mode")

Definimos la variable cl-font-lock-keywords cuyo valor por defecto es el nivel 2 de coloreado.

Indentación

Lo atractivo de un modo de Emacs, además de las funcionalidades que permite al programador, es la indentación del código. ¿Cuántas veces hemos presumido de esto con algún conocido?. Bien, ahora toca implementarlo.

La filosofía de la indentación automática es sencilla: cada vez que el usuario pulsa la tecla TAB, se debe indentar la línea en la que se encuentra el cursor. Emacs llama a una función definida por el programador del modo, para que sea ésta la que tabule correctamente la línea actual. Por tanto, debes definir una función que haga esta tarea.

En el ejemplo se llama 'cl-indent-line()':

;Tabulado
  (defun cl-indent-line ()
  "Indent current line as CL code"
  (interactive)
  (beginning-of-line)

La función la definimos como interactiva, esto es, se puede llamar 'M-x función'. Además, se especifica que el cursor está al princpio de la línea.

Comenzamos a definir la función:

;;;;Implementacion
 
  (if (bobp)
      (indent-line-to 0)

Si estamos al principio del buffer, entonces se tabula directamente a la columna 0. La función 'bobp' devuelve t (true) cuando el cursor se encuentra al principio del buffer.

(let ((not-indented t) cur-indent)

En el caso de que no estemos al princpio, entonces creamos 2 variables con la función 'let':

  • not-indented: inicializado a true y que servirá de control del bucle.
  • cur-indent: sin inicializar y contendrá el valor de indentación que se debe aplicar a la línea en cuestión tras la ejecución.

Supongamos que, cuando pulsan el TAB, nos encontramos en una línea que se compone por un }, indicando que se acaba un bloque. Si ocurre esto, entonces esa línea tiene que estar 1 tabulado menos que su línea anterior. Así:

 (if (looking-at "^[ \t]*\\(}\\)")
	  (progn
	    (save-excursion
	      (forward-line -1)
	      (setq cur-indent (- (current-indentation) cl-default-tab-width))))

El código anterior realiza lo siguiente: si en la línea en la que nos encontramos hay }, entonces nos vamos una línea más arriba (forward-line - 1) y la indentación que debemos poner es "la que haya en esa línea" - "un tabulado" (para esto utilizamos 'setq'). (current-indentation) es una función que devuelve la indentación de la línea en la que se encuentra la ejecución. De ahí que necesitemos almacenar la línea que dejamos con save-excursion y que, al terminar esta ejecución, se vuelva a la línea donde se pulsó el TAB. ¿Qué ocurriría si obviáramos save-excursion?. ;-)

La línea de progn nos sirve para incluír más de una línea en el 'then'. if en elisp tiene por defecto una línea de 'then' y otra de 'else'.

Tenemos, por tanto, contemplados ya dos casos: al principio del buffer y que sea un fin de bloque. El caso general (que sea lo que sea) lo vamos a tratar como sigue:

(save-excursion
	  (while not-indented
	    (forward-line -1)
	    (if (looking-at "^[ \t]*\\(}\\)")
		(progn
		  (setq cur-indent (current-indentation))
		  (setq not-indented nil))
	      (if (looking-at "^[ \t]*\\([a-zA-Z _()0-9]*{\\)")
		  (progn
		    (setq cur-indent (+ (current-indentation) cl-default-tab-width))
		    (setq not-indented nil))
		(if (bobp)
		    (setq not-indented nil)))))))

¡Calma! :-). Lo describimos poco a poco:

  • Vamos a la línea anterior.
  • Si lo que nos encontramos es }, entonces debemos quedarnos a la misma altura y ponemos a nil (false) la variable de control del bucle.
  • Si lo que nos encontramos es {, entonces tenemos que sumar un tabulado más y parar el bucle.
  • Si no encontramos nada y lo que llegamos es al principio del buffer, entonces salimos del bucle.
  • El proceso se repite hasta que se cumple la condición de parada, esto es, se llega a alguna de las reglas especificadas antes.

Para acabar con la función, debems hacer la tabulación que indique la variable 'cur-indent' de forma efectiva:

(if cur-indent
	  (progn
	    (if (> cur-indent 0)
		(indent-line-to cur-indent)))
	(indent-line-to 0)))))

Si está definida la varible, entonces miramos si tiene un valor lógico (tabulaciones negativas no le gustan a Emacs). En caso afirmativo aplicamos la indentación.

Si la variable 'cur-indent' no está definida o no tiene un valor positivo es que algún error ha ocurrido, por lo que indentamos directamente a 0.

Tabla de sintaxis

Ahora debemos definir una tabla sintáctica (o de sintaxis) para aquellas producciones de tu lenguaje que no se puedan definir como una expresión regular. El ejemplo típico son los comentarios del tipo /* */.

(defvar cl-mode-syntax-table
  (let ((cl-mode-syntax-table (make-syntax-table)))
    (modify-syntax-entry ?/ ". 124b" cl-mode-syntax-table)
    (modify-syntax-entry ?* ". 23" cl-mode-syntax-table)
    (modify-syntax-entry ?\n "> b" cl-mode-syntax-table)
    cl-mode-syntax-table)
  "Syntax table for cl-mode")

Configuración final

Ya estamos acabando :-). Sólo nos queda la función que se invocará cuando se cargue el modo. Esta tiene una serie de argumentos que se comentan a continuación:

(defun cl-mode ()
  "Major mode for editing CL specification"
  (interactive)
  (kill-all-local-variables)
  (use-local-map cl-mode-map)
  (set (make-local-variable 'indent-line-function) 'cl-indent-line)  
  (set (make-local-variable 'font-lock-defaults) '(cl-font-lock-keywords))
  (setq major-mode 'cl-mode)
  (setq mode-name "CL")
  (run-hooks 'cl-mode-hook))
 
(provide 'cl-mode)

  • Todo lo que hemos definido anteriormente (funciones y variables) lo asignamos a las variables correspondientes:
    • El mapa de teclado a 'use-local-map'
    • La función de indentación a 'indent-line-function'
    • La lista de coloreado a 'font-lock-defaults'
  • Como major-mode se pone el definido.
  • Se especifica el nombre que aparecerá en el buffer de Emacs (mode-name).

Enlaces y referencias

Comentarios

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.

Tu como siempre....

.... superándote a ti mismo. Tan extenso para éstos días de exámenes como lleno de posibilidades.
Mu chulo Laughing out loud

The cause of the problem is:
The vendor put the bug there.
-- Meta amigo informático --

Imagen de Lk2

Trivial

Y es que es el adjetivo que merece la receta más larga de la web por ahora (creo). Felicidades por ella... y la leeré cuando tenga tiempo.

Por cierto, algo he oído de un mode que has creado... ¿para cuando público? Laughing out loud