Моя конфигурация Emacs

;;     _   ________
;;    / | / / ____/___ ___  ____ ___________
;;   /  |/ / __/ / __ `__ \/ __ `/ ___/ ___/
;;  / /|  / /___/ / / / / / /_/ / /__(__  )
;; /_/ |_/_____/_/ /_/ /_/\__,_/\___/____/
screenshot

Я давно использую org-mode для формирования файла инициализации Emacs, периодически переписывая его с нуля и перенося в него только самое необходимое из предыдущей версии. Раньше я комментировал все на английском языке, но в сети и так полно примеров на английском, так что пусть пока будет на русском.

Так как недавно я перебрался с Gnome на i3wm, я стал использовать emacs-daemon и терминальную версию редактора, так что наведение красоты в этот раз пока сюда не попало (кое-что, тем не менее, уже добавлено).

Вот конфигурации других людей, в которых я когда-то почерпнул что-то интересное (их, вообще-то, было много больше, но остальных сейчас не припомню). Порядок - алфавитный:

Этот файл также доступен в моем блоге и на GitHub.

Запуск и инициализация

Персональная информация

Требуется, как минимум, для работы с почтой.

(setq user-full-name    "Nikolay Brovko"
      user-mail-address "[email protected]")

Настройка репозиториев и use-package

Используем Elpa, Melpa, Melpa Stable и Org.

(setq package-archives
      '(("gnu"          . "https://elpa.gnu.org/packages/")
        ("melpa"        . "https://melpa.org/packages/")
        ("melpa-stable" . "https://stable.melpa.org/packages/")
        ("org"          . "https://orgmode.org/elpa/")))

Загружаем пакет use-package, предварительно установив, если необходимо.

(unless (package-installed-p 'use-package)
  (package-refresh-contents)
  (package-install 'use-package))

(require 'use-package)

Установка библиотек

Когда требуется быстро набросать функцию, автоматизирующую то или иное действие, наличие этих библиотек под рукой сильно облегчает жизнь.

  • dash.el A modern list api for Emacs. No 'cl required.
  • s.el The long lost Emacs string manipulation library.
  • f.el Modern API for working with files and directories.
(dolist (library '(dash dash-functional s f))
  (eval `(use-package ,library :ensure t)))

Переопределение custom-file

Дабы не захламлять .emacs автоматически генерируемым мусором, перенаправляем его в другом месте.

(setq custom-file "~/.emacs.d/custom.el")
(when (file-exists-p custom-file)
  (load custom-file))

Сохранение пути к конфигу

Дабы иметь возможность удобно обращаться к другим файлам в репозитории и открывать конфиг по горячим клавишам, запомним, где что лежит.

(setq n-nemacs-file (or buffer-file-name load-file-name)
      n-nemacs-dir  (f-dirname n-nemacs-file))

Отключение бекапов и мусора возле рабочих файлов

Переносим все создаваемые редактором временные файлы и бекапы в ~/.emacs.d/backups/. Локфайлы - выключаем.

(setq backup-directory-alist
      '((".*" . "~/.emacs.d/backups/")))

(setq auto-save-file-name-transforms
      '((".*" "~/.emacs.d/backups/" t)))

(setq create-lockfiles nil)

Заменяем yes-or-no-p на y-or-n-p повсеместно

Жизнь слишком коротка, чтобы писать yes или no.

Так и не решил, относить это к инициализации или рабочему процессу, пока пусть будет здесь…

(fset #'yes-or-no-p #'y-or-n-p)

Не показывать стартовый экран

Не знаю, скольким людям он пригодился, как по мне - вещь не особенно полезная.

(setq inhibit-startup-screen 't)

Шрифт в GUI

Все таки периодически запускаю emacs в GUI, поэтому нужно установить фонт.

(let ((default-font "Iosevka 16"))
  (set-default-font default-font)
  (add-to-list 'default-frame-alist `(font . ,default-font)))

Сокрытие верхнего меню, тулбара и скроллбара

Сначала оставил менюбар, чтобы проще было находить границы окна, но во-первых, он выбивается из темы оформления, а во-вторых, границы окна лучше выделить средствами оконного менеджера, так что опять скрываю.

Тулбар и скроллбар скрываю за компанию - в терминальной версии редактора его все равно нет.

(menu-bar-mode -1)
(tool-bar-mode -1)
(scroll-bar-mode -1)

Системный буфер обмена

Поскольку я использую терминальную версию Emacs, kill-ring редактора и системный clipboard ничего друг о друге не знают. Пакет xclip решает эту проблему с помощью одноименной утилиты для командной строки.

(use-package xclip
  :ensure    t
  :config    (xclip-mode 1))

Украшательство модлайна с powerline

Временно добавлю - пока не определился, буду ли его использовать.

(use-package powerline
  :ensure    t
  :config    (powerline-center-theme))

Diminish для сокрытия минорных режимов

Зачастую случается так, что минорных режимов набирается с десяток, и в итоге они сжирают все место в модлайне. Чтобы скрывать очевидные режимы (например, включенные глобально - типа Undo Tree), используем Diminish.

(use-package diminish
  :ensure    t)

Рабочий процесс

Пробелы вместо табуляции

Не слушаем Ричарда Хендрикса и беспощадно ставим пробелы вместо табуляции.

(setq-default indent-tabs-mode nil
              tab-width        4)

Удаление слова перед курсором как в bash

Я так и не понял, что удаляет Emacs по умолчанию по нажатию C-w, но меня это не устраивает. Надо либо удалять выделенную область, либо слово перед курсором.

(defun n-kill-region-or-backward-word ()
  (interactive)
  (call-interactively
   (if (use-region-p)
       #'kill-region
     #'backward-kill-word)))

(global-set-key (kbd "C-w") #'n-kill-region-or-backward-word)

Выравнивание значений при присваивании (по символу равенства)

Я стараюсь держать символы равенства выровненными там, где это не нарушает стандарта кодирования языка или проекта. Пример ниже

foo    = "bar";
hello  = "world";
foobar = "baz";
(defun align-to-equals (begin end)
  "Align region to equal signs"
  (interactive "r")
  (align-regexp begin end "\\(\\s-*\\)=" 1 1 ))

(global-set-key (kbd "M-n M-=") #'align-to-equals)

Editorconfig

Используем editorconfig для управления кодировкой, размером и стилем отступов и т. д.

(use-package editorconfig
  :ensure    t
  :config
  (editorconfig-mode 1))

Отключение сворачивания по C-z

В терминале C-z отправляет приложение в фоновый режим, что в случае с емаксом, который сам терминал открыть может, доставляет только лишние неудобства при случайном нажатии. Кто-то предпочитает назначить на это сочетание отмену последнего действия, мне же будет спокойнее, если оно вообще ничего делать не будет.

(global-set-key (kbd "C-z") #'ignore)

Переключение буферов по C-x C-b

bs-show начал использовать еще в первые дни использования Emacs, с тех пор чего только не перепробовал, последней попыткой был helm-buffers-list, но как по мне, это менее удобно, чем открыть список буферов, в котором обычно не более 5-10 штук и клавишами n и p выбрать нужный.

(global-set-key (kbd "C-x C-b") #'bs-show)

Перемещение по буферу с помощью Ace Jump

Использовать клавиши навигации - контрпродуктивно! Ace Jump оцениваешь, попользовавшись им пару дней и поломав привычку перемещаться по тексту линейно.

(use-package ace-jump-mode
  :ensure    t
  :bind      (("C-c SPC" . #'ace-jump-mode)))

Подсветка парных скобок

Для лисповых языков, да и не только, переоценить эту функцию очень сложно…

(show-paren-mode 1)

С помощью переменной show-paren-style можно установить способ подсветки:

  • 'expression всё выражение между парными скобками
  • 'parenthesis только сами скобки
  • 'mixed смешанный вариант - в случае, если парная скобка находится в области видимости экрана, будут подсвечены только скобки, в противном случае - все выражение.

Сниппеты с yasnippet

Современные языки и фреймворки, вроде как, сильно сократили количество бойлерплейт-кода. Но, во-первых, не до нуля, а во-вторых, не все.

Добавляем директорию snippets в список директорий для поиска сниппетов. Поскольку она добавляется в начало списка, она же будет путем сохранения для сниппетов, добавляемых с помощью yas-new-snippet.

(use-package yasnippet
  :ensure t
  :config
  (progn
    (add-to-list 'yas-snippet-dirs (f-join n-nemacs-dir "snippets"))
    (yas-global-mode 1)))

Чтобы не создавать все сниппеты руками, подключаем пакет yasnippet-snippets, в который входит множество готовых сниппетов для большого количества мажорных режимов.

(use-package yasnippet-snippets
  :ensure    t
  :after     yasnippet)

Автодополнение скобок, кавычек, etc

Раньше я использовал autopair, но сейчас заглянул к нему в репозиторий и как-то там безрадостно. Случайно наткнулся на smartparens, его и буду пробовать - время покажет.

(use-package smartparens
  :ensure    t
  :diminish  smartparens-mode
  :config
  (progn
    (require 'smartparens-config)
    (smartparens-global-mode 1)))

Управление git-репозиторием с Magit

Magit - пожалуй, лучшая реализация интерфейса к git-репозиторию из всех, что мне попадались.

(use-package magit
  :ensure    t
  :bind      (("C-x g" . #'magit-status)))

Writeroom для работы над текстами

Если в процессе кодинга информация вроде имени открытого файла, текущей git-ветки, положения в документе идут строго на пользу, то при работе над постами в блог, документацией и tex-документами, она только мешает. Writeroom отображает только текущий буфер, прячет модлайн, ограничивает текст по ширине и смещает его в центр окна (не путать с выравниванием по центру). Вкупе с переводом окна в фулл-скрин и увеличением шрифта это сильно помогает сосредоточиться на тексте.

(use-package writeroom-mode
  :ensure    t
  :bind      ("M-n M-w" . #'writeroom-mode))

Neotree дерево каталогов а-ля Sublime, VSCode, etc

Вообще говоря, я как-то привык жить без него, но почему бы и нет?

(use-package neotree
  :ensure    t
  :bind      ("M-n M-n" . #'neotree-toggle))

(use-package all-the-icons
  :ensure    t
  :config
  (setq neo-theme 'icons))

Мажорные режимы для различных языков программирования и разметки

За пару месяцев их набирается полтора-два десятка, так что буду добавлять по мере использования.

Web-mode для верстки

Незаменимая вещь для работы с html, php, twig и прочими html-подобными файлами. Умеет расставлять отступы, автоматически закрывает открываемые теги, комментирует-раскомментирует блок, переименовывает тег и многое другое.

(use-package web-mode
  :ensure    t
  :config
  (progn
    (add-to-list 'auto-mode-alist '("\\.phtml\\'" . web-mode))
    (add-to-list 'auto-mode-alist '("\\.html?\\'" . web-mode))
    (add-to-list 'auto-mode-alist '("\\.twig\\'"  . web-mode))
    (add-to-list 'web-mode-engines-alist '("php"  . "\\.php\\'"))))

PHP

Тут и добавить нечего - пользуем php-mode для работы с php-кодом. Требования невысоки - нормальная подсветка синтаксиса, автоматические отступы, открытие документации. По умолчанию использовать стиль psr-2.

(use-package php-mode
  :ensure    t
  :config
  (add-hook 'php-mode-hook #'php-enable-psr2-coding-style))

JavaScript

Вообще говоря, js2 это минорный, а не мажорный режим, но и выносить его за пределы JavaScript-раздела смысла нет.

(use-package js2-mode
  :ensure    t
  :config
  (add-hook 'js-mode-hook #'js2-minor-mode))

VALA

В последнее время редактировал и писал много кода на Vala и, похоже, в будущем еще предстоит, поэтому решил перенести из локального конфига в глобальный.

(use-package vala-mode
  :ensure    t)

Т. к. в Vala принято использовать Meson для сборки проекта, подключение соответствующего режима положу сюда.

(defvar meson--project-bin nil
  "Keep last project binary to run with GDB")

(defvar meson-build-dir "build"
  "Directory which will be an argument for meson and ninja commands")

(defvar meson-auto-config t
  "If build directory isn't exists, it'll be created automatically")

(defun meson-project-root (path)
  "Return the most distant directory including meson.build file"
  (when-let ((meson-file (locate-dominating-file path "meson.build")))
    (if (locate-dominating-file (f-parent meson-file) "meson.build")
        (meson-project-root (f-parent meson-file))
      meson-file)))

(defun meson-same-project-p (path1 path2)
  "Check two paths are in same meson project"
  (and path1 path2
       (string-equal (meson-project-root path1) (meson-project-root path2))))

(defun meson-configure-project (path)
  "Create meson build directory which will be named as `meson-build-dir' value"
  (let ((default-directory path))
    (shell-command (format "meson %s" meson-build-dir))))

(defun meson-project-build-dir (path)
  "Return project build dir. Configuring project with meson if it not exists"
  (when-let (project-root (meson-project-root path))
    (let ((project-build-dir (f-join project-root meson-build-dir)))
      (unless (f-exists-p project-build-dir)
        (and meson-auto-config (meson-configure-project project-root)))
      (when (f-exists-p project-build-dir) project-build-dir))))

(defun meson-set-compile-command ()
  "Sets buffer-local `compile-command' variable"
  (when-let (project-build-dir (meson-project-build-dir (buffer-file-name)))
    (set (make-local-variable 'compile-command)
         (format "ninja -C %s" project-build-dir))))

(defun meson-set-project-bin (&optional path)
  "Ask user for current project binary path and store it in `meson--project-bin'"
  (interactive "FBinary path: ")
  (setq meson--project-bin path))

(defun meson-project-bin (path)
  "Return binary file for current project or ask it from user"
  (if (meson-same-project-p meson--project-bin path)
      meson--project-bin
    (call-interactively #'meson-set-project-bin)))

(defun meson-project-run ()
  "Run project binary"
  (interactive)
  (when-let ((project-bin (meson-project-bin (buffer-file-name))))
    (shell-command project-bin)))

(defun meson-project-run-tests ()
  "Run tests with ninja"
  (interactive)
  (when-let (project-build-dir (meson-project-build-dir (buffer-file-name)))
    (shell-command (format "ninja -C %s test" project-build-dir))))

(defun meson-project-gdb ()
  "Run project binary over GDB debugger"
  (interactive)
  (when-let* ((project-bin (meson-project-bin (buffer-file-name)))
              (project-bin-rel (f-relative project-bin
                                           (f-dirname (buffer-file-name))))
              (gdb-cmd (read-string "Run gdb (like this): "
                                    (format "gdb -i=mi %s" project-bin-rel))))
    (gdb gdb-cmd)))

(defun vala-set-meson-keys ()
  "Hook setting buffer-local Vala and Meson specific keybindings"
  (local-set-key (kbd "C-c C-c") #'compile)
  (local-set-key (kbd "C-c C-r") #'meson-project-run)
  (local-set-key (kbd "C-c C-t") #'meson-project-run-tests)
  (local-set-key (kbd "C-c C-g") #'meson-project-gdb)
  (local-set-key (kbd "C-c b")   #'meson-set-project-bin))

(use-package meson-mode
  :ensure    t
  :after     (vala-mode)
  :init
  (add-hook 'vala-mode-hook #'meson-set-compile-command)
  (add-hook 'vala-mode-hook #'vala-set-meson-keys))

Еще неплохо было бы автоматически вставлять GNU/GPL заголовок в каждый файл, если лицензия применима для текущего проекта.

(defun n-locate-dominating-file (file name)
  (when-let (dir (locate-dominating-file file name))
    (f-join dir name)))

(defun n-is-gpl-project (path)
  (when-let (copying-file (or (n-locate-dominating-file path "COPYING")
                              (n-locate-dominating-file path "LICENSE")))
    (with-temp-buffer
      (insert-file-contents copying-file)
      (search-forward "GNU GENERAL PUBLIC LICENSE" nil t))))

(defun insert-gpl-header ()
  (when-let (file (buffer-file-name))
    (unless (file-exists-p file)
      (when-let (snippet (yas-lookup-snippet "gpl3" major-mode t))
        (yas-minor-mode 1)
        (yas-expand-snippet snippet)))))

(add-hook 'vala-mode-hook #'insert-gpl-header t)

YAML

Довольно часто приходится редактировать YAML в различных конфигах. Опять же, от режима требуется, разве что, подсветка синтаксиса и сохранение отступов.

(use-package yaml-mode
  :ensure    t)

Markdown

README.md - наше все.

(use-package markdown-mode
  :ensure    t)

Жизнедеятельность

Самоорганизация

Установка свежего org-mode

Устанавливаем свежую версию org-mode c дополнениями из официального репозитория.

(use-package org-plus-contrib
  :ensure    t
  :defer     t)

Всегда включаем перенос по словам в длинных строках для org-mode (практика показала, что выключать его приходится многократно реже, чем включать).

(add-hook 'org-mode-hook 'visual-line-mode)

Настройка шаблонов для org-capture

org-capture это крайне полезная штука, позволяющая быстро делать заметки и комментарии из любого места, сохраняя их в указанных местах. В моем случае org-capture вызывается командой C-c c.

(global-set-key (kbd "C-c c") #'org-capture)

Мои org-файлы обычно лежат в папке ~/Org, которая, в свою очередь, является симлинком на папку Org в облаке.

(setq n-org-files-dir "~/Org"
      n-org-organizer (f-join n-org-files-dir "organizer.org")
      n-org-movies    (f-join n-org-files-dir "movies.org")
      n-org-logbook   (f-join n-org-files-dir "logbook.org.gpg"))
(setq org-default-notes-file n-org-organizer)

Шаблоны, которые я использую:

  • i inbox Попадает во Входящие для последующего разбора, схоже с GTD.
  • t todo Попадает во Входящие в виде TODO-заметки для последующего разбора, схоже с GTD.
  • m movie Сохраняет фильм для последующего просмотра. При вводе спрашивает название, жанр, год, ссылку на описание.
  • l logbook Используется для фиксации различных событий и мыслей в шифрованном файле, отдаленно напоминает дневник, только не дневник.
(setq n-capture-templates-dir (f-join n-nemacs-dir "org-templates"))
(setq org-capture-templates
      `(("i" "Plain inbox entry"
         entry  (file+headline ,n-org-organizer "Входящие")
         (file ,(f-join n-capture-templates-dir "inbox-plain.org")))
        ("t" "Todo inbox entry"
         entry  (file+headline ,n-org-organizer "Входящие")
         (file ,(f-join n-capture-templates-dir "inbox-todo.org")))
        ("m" "Movie to watch"
         entry  (file+headline ,n-org-movies "All movies")
         (file ,(f-join n-capture-templates-dir "movie.org")))
        ("l" "Logbook entry"
         entry  (file+olp+datetree ,n-org-logbook)
         (file ,(f-join n-capture-templates-dir "logbook.org")))))

Настройка org-refile

После того, как заметки попали в Inbox, их периодически нужно разбирать, раскладывая по соответствующим разделам и файлам. Для этого служит механизм org-refile. Настроим целевые файлы, в которые возможен перенос. Сам Inbox по понятным причинам в этот список не входит.

(setq org-refile-targets `((,n-org-organizer . (:maxlevel . 2))))

Настройка org-agenda

Когда в заметках наведен порядок, хотелось бы в удобоваримом виде получить список дел на день, неделю или без привязки к календарю. Для этого есть org-agenda. Указываем файлы, из которых нужно собирать заметки и назначаем клавиши вызова.

(setq org-agenda-files (list n-org-organizer))
(global-set-key (kbd "C-c a") #'org-agenda)

Настройка org-archive

Удаление выполненных или отмененных дел - плохая практика т. к. иногда (не так уж редко) бывает нужно уточнить, что именно было сделано, когда, если отменено - то почему и т. д. Короче говоря, айтишники тяжело расстаются с метаинформацией. Поэтому вместо удаления лучше использовать org-archive, который позволяет перемещать ставшие ненужными заметки или деревья в отдельный файл, добавляя к ним информацию о дате архивирования, предыдущем местонахождении и др.

Чтобы файл не мозолил глаза, я использую скрытый файл .archive.org, а заметки в нем раскладываются по разделам с именами файлов, из которых они были перемещены.

(setq org-archive-location (f-join n-org-files-dir
                                   ".archive.org::** Из файла %s"))

Метод помидора (pomidor)

По правде сказать, я не так часто пользуюсь этим методом, но иногда все же пользуюсь. Поэтому пусть будет на готове.

(use-package pomidor
  :ensure    t
  :bind      (("M-n M-p" . #'pomidor))
  :config
  (setq pomidor-play-sound-file #'ignore
        alert-default-style     'libnotify))

Почта (Gnus)

Вообще, для работы с почтой я использую neomutt. Но поскольку я подписан на множество списков рассылки, где часто присылают код, либо самому приходится отправлять фрагменты кода, сразу использовать редактор бывает очень удобно.

Извлечением почты у меня занимается offlineimap, каждые две минуты по крону забирающий почту с сервера по IMAP и аккуратно складывающий ее в папку ~/.mail в формате Maildir. Помимо того, что это позволяет более полноценно работать с почтой, выбирая инструмент под задачу (иногда просто шелл), так появляется возможность разбирать почту из оффлайна - для десктопа так себе достижение, а для ноутбука бывает очень полезно.

(setq gnus-select-method '(nnmaildir "nickey" (directory "~/.mail")))

Ну а поскольку можно разбирать почту в оффлайне, почему бы ее в оффлайне не писать? Я использую msmtp для отправки почты. В пакете с msmtp идут скрипты msmtp-enqueue.sh, msmtp-listqueue.sh и msmtp-runqueue.sh для работы с очередью отправки. Использую msmtp-enqueue для отправки почты, а msmtp-runqueue запускается по крону для отправки почты.

(setq message-send-mail-function #'message-send-mail-with-sendmail
      sendmail-program           "~/.local/bin/msmtp-enqueue")

По умолчанию Gnus начинает хозяйничать в домашней папке и создавать в ней папки Mail и News. Спрячем их в глубинах ~/.emacs.d.

(setq message-directory  "~/.emacs.d/mail/"
      gnus-directory     "~/.emacs.d/news/"
      nnfolder-directory "~/.emacs.d/mail/archive/")