Программисты делятся на две категории:
1) Те, кто уже использует Vim.
2) Те, кто уже использует Emacs.
3) Те, кто ещё не использует.


Предисловие


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

Добавление режима


В Emacs'е есть множество разных режимов, добавляющих функциональность в него. Как правило, когда нужна какая-то фича, она скачивается в виде пакета, состоящего из файлов .el (Emacs Lisp), и они уже подключаются к встроенным .el файлам, отвечающим за загрузку редактора.

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

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

Чем дольше работаешь с Emacs'ом, тем больше он нравится. Поэтому мотивация не заставила себя долго ждать — появилась задача: «Мне нужно иметь какую-то коробочку с инструментами для автоматического форматирования ответов на форум.»

Как решить (естественно, не выключая Emacs)?

Ответ прост: нужно добавить свой режим, который можно включить, когда нужен, и отключить в обратном случае.

Обратная инженерия


Естественно, для добавления режима нужно прочитать кучу документации, посмотреть, как сделаны другие режимы, изучить лисп и случайно не запортачить что-нибудь в работающем Emacs'е.

Начались поиски документации. Сначала туториальные статьи… статей нет. Тогда примеры режимов… примеры переполнены лишними конструкциями. Тогда документация… это читать и проверять несколько дней. Тогда готовые режимы… ничего не понятно, надо учить лисп.

Круг замкнулся. Пришлось использовать всё сразу и много.

Первое приближение


Информация:

1) Режимы бывают главные (major) и побочные (minor).
2) Режимы могут наследоваться от существующих режимов.
3) Главный режим может быть выбран только один, тогда как побочных — много.
4) К главному режиму можно прицепить множество побочных.

Сперва был составлен самый минимальный режим:
;; My test mode

(define-derived-mode test-mode python-mode "Test"
  "Major mode for editing Test source text."
  (set (make-local-variable 'test-variable) "Test variable value"))

(add-to-list 'auto-mode-alist (cons "\\.test\\'" 'test-mode))

(provide 'test-mode)

Файл test-mode.el с этим текстом кладётся в удобную папку, а затем режим подключается где-нибудь в файле инициализации, как и любой другой.

(add-to-list 'load-path "~/.emacs.d/packages/modes/test-mode/")
(require 'test-mode)


Объяснение содержимого:
Создаём режим, наследующий всё из питоновского режима:

(define-derived-mode test-mode python-mode

Эта строка будет показываться в полоске названия режима:

 "Test"

Это комментарий, который показывается, когда открываешь помощь для режима:

"Major mode for editing Test source text."

Это переменная, которая создаётся при входе в режим и разрушается при выходе из него:

  (set (make-local-variable 'test-variable) "Test variable value"))

Это назначает файлам с расширением .test этот режим автоматически:

(add-to-list 'auto-mode-alist (cons "\\.test\\'" 'test-mode))

Это расшаривает режим, чтобы его можно было подключить из других файлов:

(provide 'test-mode)


С минимумом закончили.

Как этим всем пользоваться?

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

Добавляем функцию и назначаем клавишу для неё:
;; My test function mode

(defun test-func-hello()
  (interactive)
  (message "Pressed C-c 1, test-func-hello()"))

(defvar test-func-mode-map
   (let ((map (make-sparse-keymap)))
     (define-key map (kbd "C-c 1") 'test-func-hello)
     map)
   "Keymap for `test-func-mode'.")

(define-derived-mode test-func-mode python-mode "TestFunc"
  "Major mode for editing TestFunc source text."
  (set (make-local-variable 'test-func-variable) "TestFunc variable value"))

(add-to-list 'auto-mode-alist (cons "\\.testfunc\\'" 'test-func-mode))

(provide 'test-func-mode)


Здесь изменено только имя режима и расширение файлов, в которых он будет включаться автоматически. И ещё добавлено две штуки: функция и отображение клавиш.

Когда мы создаём файл file.testfunc и открываем его в Emacs'е, в нём устанавливается режим TestFunc. Так как режим унаследован от питоновского, у нас работает подсветка синтаксиса и другие вещи. Но при нажатии Ctrl + c + 1 у нас высвечивается сообщение о том, что нажата эта комбинация, и имя сработавшей функции.

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

Минимальное связывание


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

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

Переделываем главный режим с функцией в побочный:
;; My test minor mode

(defun test-minor-hello()
  (interactive)
  (message "Pressed C-c 1, test-minor-hello()"))

(defvar test-minor-mode-map
   (let ((map (make-sparse-keymap)))
     (define-key map (kbd "C-c 1") 'test-minor-hello)
     map)
   "Keymap for `test-minor-mode'.")

(define-minor-mode test-minor-mode
  "Minor mode for editing TestMinor text."
  nil
  " TestMinor"
  nil
  (if test-minor-mode
      (set (make-local-variable 'test-minor-variable)
           "TestMinor variable value")
    (makunbound 'test-minor-variable)))

(add-to-list 'auto-mode-alist (cons "\\.testminor\\'" 'test-minor-mode))

(provide 'test-minor-mode)


Как видно, здесь всё не так радостно, как при создании главного режима. Появились какие-то странные конструкции, совсем не понятные.

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

Если объяснять по-сложному, в Emacs'е зашито определённое поведение для работы с такими режимами. Можно было их сделать лучше, но уже поздно.

Главное, что нужно помнить про малые режимы, — у них вверху четыре переменные. Там можно случайно это упустить, и он выражение, которое должно выполняться при включении режима, подставит вместо последней переменной, которая никак об этом не сообщит — будешь два часа сидеть в документации, чтобы понять, почему не работает выражение.

Решение поставленной задачи


Конечный вариант
;; Forum minor mode

(defun forum-minor-wrap-code()
  (interactive)
  (if (not (mark))
      (set-mark (point)))
  (narrow-to-region (mark) (point))
  (goto-char (point-min))
  (insert "<code>")
  (goto-char (point-max))
  (insert "</code>")
  (set-mark (point-min))
  (widen))

(defun forum-minor-wrap-quote()
  (interactive)
  (if (not (mark))
      (set-mark (point)))
  (narrow-to-region (mark) (point))
  (goto-char (point-min))
  (insert "<quote>")
  (goto-char (point-max))
  (insert "</quote>")
  (set-mark (point-min))
  (widen))

(defvar forum-minor-mode-map
   (let ((map (make-sparse-keymap)))
     (define-key map (kbd "C-c 1") 'forum-minor-wrap-quote)
     (define-key map (kbd "C-c 2") 'forum-minor-wrap-code)
     map)
   "Keymap for `forum-minor-mode'.")

(define-minor-mode forum-minor-mode
  "Minor mode for editing text for `http://forum.example.ru'."
  nil
  " Forum"
  nil
  (if forum-minor-mode
      (set (make-local-variable 'forum-minor-nick)
           "Nick")
    (makunbound 'forum-minor-nick)))

(add-to-list 'auto-mode-alist (cons "\\.forum\\'" 'forum-minor-mode))
(add-to-list 'yas-extra-modes 'forum-minor-mode)

(provide 'forum-minor-mode)


Здесь добавлена пара функций для оборачивания в теги <quote></quote> и <code></code> выделенного текста. К тому же пришлось добавлять режим в yasnippet, потому что тот обнаруживает только главные режимы.

Здесь нужно отметить, что режим можно привязать к файлу, запустить вручную через Alt + x + название, либо прицепить к другому режиму через хук.

Вот так выглядит присоединение к питоновскому:

(add-hook 'python-mode-hook 'forum-minor-mode)

Теперь можно заняться написанием более продвинутых функций по обработке текста. Режимы можно добавлять и удалять, не влияя на остальные настройки.

Заключение


Когда я поставил Emacs в первый раз, то не понял его, он показался мне сложным, неудобным и некрасивым (из-за древних шрифтов). Теперь же, поставив его во второй раз, я его не узнал. В нём оказалось так много чудесных приложений, а удобство зашкаливает.

До него я пользовался Vim'ом, это было хорошо и интересно, но его настройки казались какими-то неродными, всё время нужно было читать что-то, чтобы понять мнемонику. Из-за этого потом долго программировал в kwrite, потому что изучать ничего не надо, а синтаксис выравнивает и подкрашивает хорошо.

Но только установив и влившись в Emacs, я понял, что такое мощь и простота (до поры до времени, конечно, пока не попробуешь открыть файл на 20Мb).

P.S.: Если что неправильно во мною приведённом коде или подходе — велком.

Комментарии (7)


  1. Joric
    13.10.2015 14:51
    +2

    Программисты делятся на две категории
    В троичной системе, видимо.


    1. cyber_genius
      13.10.2015 15:25
      +1

      нда, и боюсь emacs такой же древний как и троичная система, но надо всё равно его попробовать


  1. MAXH0
    13.10.2015 18:49

    Работаю в emacs преимущественно в консольном режиме (проклятая ностальгия).
    Единственно что напрягает — костыли с проверкой орфографии. Я понимаю что это не основное применение этого редактора, но все же по умолчанию — все довольно неудобно, а пилить нет времени и сил.


    1. samo-delkin
      14.10.2015 04:44
      +1

      Работаю в emacs преимущественно в консольном режиме (проклятая ностальгия).

      Я в git'е его юзаю в консоли

      .gitconfig
      [core]
              editor = emacs --no-window-system
      

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

      Я понимаю что это не основное применение этого редактора, но все же по умолчанию — все довольно неудобно

      Я, наоборот, ставил себе целью изучить умолчания, поэтому проигнорировал и переделывание клавиш, и стартеркиты.
      Просто попадёшь на комп с линем, где есть Emacs, но нет никаких настроек, и будешь связан по рукам и ногам.


      1. roman_kashitsyn
        14.10.2015 11:04
        +1

        Я в git'е его юзаю в консоли

        Если взять привычку запускать Emacs в режиме сервера, то можно сделать git-редактором emacsclient, он стартует очень быстро.

        Ещё можно поставить magit и (практически) не переключаться на консольный гит. Magit во многом удобнее родного интерфейса git (да, я сам когда-то не подозревал, что такое возможно).


        1. samo-delkin
          14.10.2015 13:40

          Если взять привычку запускать Emacs в режиме сервера, то можно сделать git-редактором emacsclient, он стартует очень быстро.

          Когда ставил, рассматривал этот вариант, не в плане git'а, а вообще. Тоже не стал юзать.
          Очень часто держу несколько открытых Emacs'ов, в каждом по множеству окон, всё изолировано.

          Ещё можно поставить magit и (практически) не переключаться на консольный гит.

          Я ещё не изучал системы контроля версий в Emacs'е, на видео сейчас глянул magit.

          Сейчас у меня gitk (красивое дерево коммитов со всей инфой) и множество самодельных алиасов.

          [alias]
                  co = checkout
                  ci = commit
                  st = status
                  cf = config --global
                  br = branch
                  mg = merge
                  sh = stash
                  rs = reset
                  fp = format-patch
                  ar = archive
                  ds = describe
                  sl = shortlog
                  rl = reflog
                  chp = cherry-pick
                  bs = bisect
                  sm = submodule
                  fb = filter-branch
                  logt = log --oneline --graph --decorate --all
                  logp = log -p
                  logfmt = log --pretty=format:\"%h  %cr  %cn <%ce>%n%s%n\"
                  logd = log --relative-date
                  logo = log --oneline
                  logo1 = log --oneline -n 1
                  logolr = log --oneline --left-right
                  logs = log --stat
                  logsp = log -p --stat
                  logps = log -p --stat
                  log1 = log -1
                  log1p = log -p -1
                  logp1 = log -p -1
                  logg = log -g
                  diffc = diff --cached
                  diffh = diff HEAD
                  diffk = diff --check
                  diffck = diff --cached --check
                  cim = commit -m
                  cia = commit --amend
                  cic = commit --reset-author -c
                  cob = checkout -b
                  cof = checkout -f
                  brv = branch -vv
                  brva = branch -avv
                  brd = branch -d
                  brdf = branch -D
                  brm = branch -m
                  mgb = merge --no-ff
                  addp = add -p
                  addi = add -i
                  rmv = remote -v
                  rms = remote show
                  rmsu = remote set-url
                  tagd = tag -d
                  tagn = tag -n
                  vi = !gitk --all
                  vib = !gitk
                  shl = stash list
                  sha = stash apply
                  shai = stash apply --index
                  shp = stash pop
                  shpi = stash pop --index
                  shd = stash drop
                  shsp = stash show -p
                  shb = stash branch
                  unstash = !git stash show -p | git apply -R
                  dst = describe --tags
                  rmc = rm --cached
                  bsb = bisect bad
                  bsg = bisect good
                  bss = bisect start
                  bsr = bisect run
                  bsrs = bisect reset
                  fbf = filter-branch -f
                  sma = submodule add
                  smui = submodule update --init
                  smu = submodule update
                  cfu = config --global --unset
                  reco = "!git reset -q; git co *"
          


          И вот это всё надо чтобы там работало, потому что я к этому привык. А с magit'ом, походу, придётся там изучать много нового, а потом ещё что-нибудь невозможно будет воспроизвести.

          Интерес есть, но работа пока в таком режиме, когда всё привычно.


          1. roman_kashitsyn
            15.10.2015 16:48

            А на этот случай у меня проездной M-x shell :)
            Я использую magit для большинства задач, изредка переключаясь в *shell*, чтобы выполнить то, что не умеет magit.