Saltar para o conteúdo

Como construir um plugin para GNU Emacs

Fonte: Wikiversidade

O que é GNU Emacs?

[editar | editar código-fonte]

GNU Emacs é a variante mais utilizada de uma família de editores de texto que se destacam por sua extensibilidade, conhecidos como EMACS[1][2]. Desenvolvido em 1984 por Richard Stallman[3], o GNU Emacs se destaca dos demais de sua família por ser um marco no movimento do software livre e um componente central do projeto GNU.

O GNU Emacs, assim como outros da família, tem muitas de suas funcionalidades implementadas em um dialeto de Lisp, conhecido como Emacs Lisp[4]. Portanto, sua customização se dá por meio de código escrito nessa linguagem, que é primariamente interpretada, e o GNU Emacs possui um interpretador de GNU Emacs Lisp. Assim, o desenvolvedor tem à sua disposição uma linguagem de programação completa para desenvolver plugins para esse editor de texto.

Fundamentos de Lisp e GNU Emacs Lisp

[editar | editar código-fonte]

Conceitos importantes de Lisp

[editar | editar código-fonte]

A sintaxe de Lisp é principalmente composta de listas, símbolos e valores, e código pode também ser tratado como dados comuns. Por exemplo, a seguinte função:

(defun print-text(text)
  message "%s" text))

Internamente é entendida como uma lista com os elementos defun print-text, e com uma lista aninhada text. Essa característica de Lisp possibilita metaprogramação, que é importante para a customização de funções do editor de texto.

Particularidades do Emacs Lisp

[editar | editar código-fonte]

Primeiramente, é válido destacar os tipos específicos do GNU Emacs Lisp, tais como buffer, window, frame, thread, keymap, entre outros. Esses tipos, dos quais a maioria necessita de uma API para obtê-los, diferente dos tipos primitivos de Lisp, permitem modificar a interface do GNU Emacs — que por sua vez é um editor de texto com interface gráfica (GUI), diferente de outros como o vi e o vim.

Além disso, GNU Emacs Lisp possui uma particularidade de namespaces, possuindo um exclusivo para variáveis, e um exclusivo para funções, de modo que é possível termos uma variável e uma função com o mesmo nome nessa linguagem.

Buffers no GNU Emacs

[editar | editar código-fonte]

Um buffer no GNU Emacs é um objeto que contém texto a ser exibido ou editado. O texto que está contido num buffer pode tanto ser originado de um arquivo quanto gerado dentro do próprio GNU Emacs (como o minibuffer, um buffer especial para executar comandos no GNU Emacs).

Além de texto, buffers também possuem parâmetros que controlam a cor do texto, fonte, tamanho, entre outros. Adicionalmente, buffers podem conter um conjunto próprio de variáveis que definem o comportamento de códigos que rodarem no contexto daquele buffer (esse conceito será importante para entender modes).

Lisp oferece um mecanismo para avaliar expressões, e cada elemento da linguagem é avaliado de uma maneira.

  • Valores primitivos são autoavaliados:
10

"texto qualquer"

Ao avaliarmos as expressões acima — no GNU Emacs, isso pode ser feito com o comando C-x C-e —, notamos que o resultado são elas mesmas.

  • Símbolos retornam seu valor quando avaliados:
buffer-file-name

Ao avaliarmos o símbolo acima, obtemos seu valor, que no ambiente do GNU Emacs corresponde ao arquivo que está aberto no buffer atual. Símbolos podem ser valores (variáveis) ou funções.

  • Listas são avaliadas de acordo com o primeiro símbolo que aparece nelas:
(+ 3 2)

Ao avaliarmos a lista acima, o interpretador primeiro avalia o primeiro símbolo +, que corresponde a uma função, e então avalia essa função passando como parâmetro os valores seguintes 3 2, retornando 5.

Uma maneira rápida de escrever código em Emacs Lisp e avaliá-lo dentro do GNU Emacs é por meio do comando M-:.

Construindo um plugin para GNU Emacs

[editar | editar código-fonte]

Veremos agora um exemplo de como construir um plugin para GNU Emacs criando um minor mode.

Modes no GNU Emacs são modos do editor que oferecem ao usuário certas funcionalidades quando estão ativos, e podem ser divididos em dois tipos:

  • Major modes: são o mode principal de um buffer, e apenas um pode estar ativo por vez (por exemplo: org-mode).
  • Minor modes: são modes que proveem funcionalidades extras em um buffer ou até mesmo globalmente no GNU Emacs. Mais de um pode estar ativo por vez (por exemplo: auto-fill-mode).

As funcionalidades que um mode provê podem ser:

  • Um keymap que só é ativado se o mode em questão está ativo.
  • Um indicador no mode line que mostra se o mode está ativo.
  • Um hook, que executa outras funções ou modes quando o mode em questão é ativado.
  • Código que é executado quando se ativa ou desativa o mode em questão.

Requisitos para criar um minor mode

[editar | editar código-fonte]

Um minor mode precisa cumprir os seguintes requisitos para funcionar corretamente no GNU Emacs:

  • Uma variável para indicar se o mode está ativo:
    • O nome dessa variável deve terminar em -mode
    • O valor nil indica que o mode está desativado, e qualquer outro valor indica que está ativado.
  • Um comando para habilitar e desabilitar o mode:
    • Esse comando deve ter o mesmo nome da variável que indica se o mode está ativo ou não.
    • O comando pode realizar quaisquer mudanças necessárias no estado local do buffer ou global do GNU Emacs para que o mode funcione como esperado.
    • O comando deve previamente checar se o mode já está ativo ou não.

Opcionalmente, o minor mode pode também conter:

  • Um keymap para o mode:
    • Para criar um keymap específico desse mode, basta adicionar um par chave-valor no minor-mode-map-alist, que registra todos os keymaps de todos os modes do GNU Emacs.
  • Um indicador de que o mode está ativo:
    • Para isso, basta registrar o mode no minor-mode-alist junto com a string a ser mostrada no mode line quando ele estiver ativo.

Iremos construir do zero um minor mode com uma única funcionalidade (um keymap), e registrá-lo na lista de minor modes do editor de texto.

  • Primeiramente, criemos uma variável para representar se nosso minor mode está ativo:
(make-variable-buffer-local
  (defvar example-mode nil
    "Toggle example-mode."))

Note que aqui usamos make-variable-buffer-local, de forma que permitimos valores diferentes para essa variável a depender do buffer em que estamos, o que nos permite utilizar esse mode apenas nos buffers que desejarmos. Além disso, estamos por padrão deixando o mode desativado (pois passamos o valor de inicialização nil para a variável)

  • Em seguida, criemos uma variável para o keymap do nosso mode:
(defvar example-mode-map (make-sparse-keymap)
  "Keymap for test-mode.")

Aqui, utilizamos a função make-sparse-keymap para criar um objeto do tipo keymap, específico do GNU Emacs.

  • Agora, criemos uma keybinding que será acessível quando nosso mode estiver ativo:
(define-key example-mode-map (kbd "C-c C-p")
  (lambda()
    (interactive)
    (message "hello, world!")))

Nesse trecho, relacionamos uma sequência de comandos de teclado com uma função lambda que imprime "hello, world!" no minibuffer. Utilizamos a anotação (interactive) para que o GNU Emacs reconheça essa função como um comando que pode ser executado via a keybinding definida, ou com M-x nome da função, caso ela tenha um nome. No nosso caso, temos uma função anônima (lambda), e portanto ela somente pode ser executada via comandos de teclado.

  • Como já definimos as funcionalidades do nosso minor mode, vamos registrá-lo para que o GNU Emacs o reconheça como um dos modes disponíveis:
(add-to-list 'minor-mode-alist '(example-mode " example"))
(add-to-list 'minor-mode-map-alist (cons 'example-mode example-mode-map))

Aqui registramos nosso mode no minor-mode-alist junto com a string a ser mostrada no mode-line quando ele estiver ativo.

  • Agora, definamos uma função para ativar ou desativar o mode que criamos:
(defun example-mode (&optional ARG)
  (interactive (list 'toggle))
  (setq example-mode
    (if (eq ARG 'toggle)
      (not example-mode)
    (> ARG 0)))

Nesse trecho, definimos um argumento ARG opcional para ativar ou desativar nosso mode — por convenção, valores maiores que 0 ativam um mode, e valores menores ou iguais a 0 desativam-no.

Em seguida, usamos novamente a anotação (interactive) para que essa função possa ser executada via M-x ou keybinding, e passamos o argumento padrão 'toggle para ela caso seja invocada interativamente.

Por fim, definimos o valor da variável example-mode baseado no argumento ARG passado. Caso seja igual à 'toggle, então o mode é desativado caso esteja ativado e vice-versa. Caso ARG seja um número, utilizamos a convenção para ativarmos ou desativarmos o mode: a expressão (> ARG 0) retorna t caso verdadeira, e nil, caso falsa.

  • Para finalizar, definamos comportamentos para quando ativamos ou desativamos o mode:
(if example-mode
  (message "mode activated!")
  (message "mode deactivated...")))

Com isso, criamos do zero um minor mode para GNU Emacs que pode ser utilizado em qualquer buffer do editor.

Próximos passos

[editar | editar código-fonte]

Para aprender mais sobre Emacs Lisp e construção de extensões para GNU Emacs, é recomendado estudar as implementações de comandos built-in do editor, que são, em sua maioria, implementados nessa linguagem.

Para isso, basta executar o comando:

C-h x nome-do-comando RET

Aqui um exemplo da implementação da função switch-to-buffer:

  (interactive
   (let ((force-same-window
          (unless switch-to-buffer-obey-display-actions
            (cond
             ((window-minibuffer-p) nil)
             ((not (eq (window-dedicated-p) t)) 'force-same-window)
             ((pcase switch-to-buffer-in-dedicated-window
                ('nil (user-error
                       "Cannot switch buffers in a dedicated window"))
                ('prompt
                 (if (y-or-n-p
                      (format "Window is dedicated to %s; undedicate it?"
                              (window-buffer)))
                     (progn
                       (set-window-dedicated-p nil nil)
                       'force-same-window)
                   (user-error
                    "Cannot switch buffers in a dedicated window")))
                ('pop nil)
                (_ (set-window-dedicated-p nil nil) 'force-same-window)))))))
     (list (read-buffer-to-switch "Switch to buffer: ") nil force-same-window)))
  (let ((buffer (window-normalize-buffer-to-switch-to buffer-or-name))
        (set-window-start-and-point (not switch-to-buffer-obey-display-actions)))
    (cond
     ;; Don't call set-window-buffer if it's not needed since it
     ;; might signal an error (e.g. if the window is dedicated).
     ((and (eq buffer (window-buffer))
           ;; pop-to-buffer-same-window might decide to display
           ;; the same buffer in another window
           (not switch-to-buffer-obey-display-actions)))
     ((and (window-minibuffer-p)
           (not switch-to-buffer-obey-display-actions))
      (if force-same-window
          (user-error "Cannot switch buffers in minibuffer window")
        (pop-to-buffer buffer norecord)))
     ((and (eq (window-dedicated-p) t)
           (not switch-to-buffer-obey-display-actions))
      (if force-same-window
          (user-error "Cannot switch buffers in a dedicated window")
        (pop-to-buffer buffer norecord)))
     (t
      (when switch-to-buffer-obey-display-actions
        (let ((selected-window (selected-window)))
          (pop-to-buffer-same-window buffer norecord)
          (when (eq (selected-window) selected-window)
            (setq set-window-start-and-point t))))

      (when set-window-start-and-point
        (let* ((entry (assq buffer (window-prev-buffers)))
               (preserve-win-point
                (buffer-local-value 'switch-to-buffer-preserve-window-point
                                    buffer))
	       (displayed (and (eq preserve-win-point 'already-displayed)
			       (get-buffer-window buffer 0))))
	  (set-window-buffer nil buffer)
	  (when (and entry (or (eq preserve-win-point t) displayed))
	    ;; Try to restore start and point of buffer in the selected
	    ;; window (Bug#4041).
	    (set-window-start (selected-window) (nth 1 entry) t)
	    (set-window-point nil (nth 2 entry)))))))

    (unless norecord
      (select-window (selected-window)))
    (set-buffer buffer)))
  1. GNU Emacs FAQ. https://www.gnu.org/software/emacs/manual/html_mono/efaq.html#Origin-of-the-term-Emacs
  2. Cameron, Debra. GNU Emacs Pocket Reference. O'Reilly. https://www.oreilly.com/library/view/gnu-emacs-pocket/1565924967/
  3. About the GNU Project. https://web.archive.org/web/20240903201004/https://www.gnu.org/gnu/thegnuproject.html
  4. Emacs Lisp. https://en.wikipedia.org/wiki/Emacs_Lisp