К чему эти прыжки?
                 Остап Бендер

Вступление


Вообще-то я хотел написать небольшую заметку «о некоторых особенностях работы с макросами в Clojure». Но попутно решил наконец более основательно ознакомиться с Emacs.

Я конечно не совсем ровестник Lisp, однако знакомы мы вот уже… дцать лет и потенциал этого замечательного языка (даже скорее философии) вполне себе представляю и в теории и на практике. Было дело писал и свои реализации (скорее для лучшего понимания механизмов работы интерпретатора Lisp нежели для практического использования). Однако, Emacs практически не использовал т.к. в стародавние времена достаточно плотной работы с Lisp вполне обходился встроенным редактором моей версии (muLisp, редактор конечно же тоже был написан на нём самом). Потом приходилось работать с «более другими» инструментами, а последние годы и вовсе в иной сфере. Сейчас вот появилось немного времени «для души»…

Собственно «погружение» в Emacs прошло вполне комфортно — хотя я (почему-то всё ещё) и не юниксоид, но к консольным командам и вообще работе с клавиатурой отношусь с пониманием. Настройка управления и джентльменского набора «плагинов» также не вызвала проблем. С прикручиванием SBCL, Clojure и Scala пришлось немного повозиться, но всему виной было несоответствие версий и/или их (версий) врождённые проблемы.

Однако синдром «прыгающего курсора» (перемещение к концу строки при переходе к следующей/предыдущей строке в случае, если она короче текущей) вызывает лёгкую идиосинкразию. Если бы дело шло не о Emacs, то скорее всего пришлось бы смириться и искать «концептуальность» в таком подходе, как это часто делается при невозможности решения проблем. Но, поскольку мы имеем дело с конструктором редакторов, то проблема была трактована как вызов (как сейчас стало модно говорить).

Итак оставим в стороне сам вопрос необходимости «дрессировки» курсора — лично для меня это «50 на 50» удобство и просто хорошая физика неплохое упражнение для ознакомления с концепцией программирования Emacs. Возможно кому-то тоже будет интересно и полезно, по крйней мере «на вскидку» готовых решений я не нашёл (только вопросы «а как сделать?..») — допускаю, что не сильно искал по причине см. выше.

Целевая аудитория


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

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

Решение


Собственно, моё решение достаточно очевидно:
  • поскольку мы можем перемещать курсор только в терминах буфера, то нам потребуется дополнять строки «лишними» пробелами для того, чтобы можно было переместить курсор в нужную позицию;
  • так как эти пробелы в самом деле лишние, необходимо их удалять при первой же возможности;
  • оформить функционал удобнее всего в качестве minor mode.

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

Итак, приступим к созданию minor mode который будет называться скажем wpers (далее по тексту просто режим). Полный текст пакета с краткой инструкцией по установке вы можете получить с GitHub. Здесь я подробно прокомментирую весь код.

Концептуально, всё, что нам необходимо — это перехватить команды next-line, previous-line, right-char и move-end-of-line. Первые три просто должны добавлять пробелы, при необходимости, последняя будет автоматически удалять «лишние» пробелы в конец строки. Перехват осуществляется стандартными средствами remap в рамках key-map-а локального для режима.

По возможности вынесем всё что можно из кода в константы:
;; Функции (команды) которые мы будем перехватывать
(defconst wpers-overloaded-funs [next-line previous-line right-char move-end-of-line] "Functions overloaded by the mode")

;; Префикс к именам перехватываемых ("старых") функций 
;; для получения имён функций их перехватывающих ("новых")
(defconst wpers-fun-prefix "wpers-" "Prefix for new functions")

;; Ассоциативный список - отображение из "старых" имён функций в "новые"
(defconst wpers-funs-alist
  (mapcar '(lambda (x) (cons x (intern (concat wpers-fun-prefix (symbol-name x)))))
          wpers-overloaded-funs)
  "alist (old . new) functions")

;; Key-map режима - суть перехват раннее декларированного  набора функций (wpers-overloaded-funs)
(defconst wpers-mode-map
  (reduce '(lambda (s x) (define-key s (vector 'remap (car x)) (cdr x)) s)
          wpers-funs-alist :initial-value (make-sparse-keymap))
  "Mode map for `wpers'")

;; Ассоциативный список - отображение из переменных-хуков (событий) в их обработчики
(defconst wreps-hooks-alist
  '((pre-command-hook . wpers--pre-command-hook)
    (auto-save-hook   . wpers-kill-final-spaces)
    (before-save-hook . wpers-kill-final-spaces))
  "alist (hook-var . hook-function)")


Далее, определим сам режим:
(define-minor-mode wpers-mode
  "Toggle persistent cursor mode."
  :init-value nil
  :lighter " wpers"
  :group 'wpers
  :keymap wpers-mode-map
  (if wpers-mode
      (progn
        (message "Wpers enabled")
        (mapc '(lambda (x) (add-hook (car x) (cdr x) nil t)) wreps-hooks-alist)) ; добавляем свои обработчик событий
      (progn
        (message "Wpers disabled")
        (wpers-kill-final-spaces)
        (mapc '(lambda (x) (remove-hook (car x) (cdr x) t)) wreps-hooks-alist)))) ; удаляем свои обработчик событий


Теперь собственно нехитрая «кухня» функционала режима:
;; Выполнение заданной формы (form) с восстановлением исходной позиции курсора в строке (столбца)
(defmacro wpers-save-vpos (form) "Eval form with saving current cursor's position in the line (column)"
  (let ((old-col (make-symbol "old-col")))
    `(let ((,old-col (current-column)) last-col) ,form (move-to-column ,old-col t))))

;;; Двигаем курсор вверх/вниз с сохранением позиции по вертикали

(defun wpers-next-line () "Same as `new-line' but adds the spaces if it's needed
for saving cursor's position in the line (column)"
  (interactive) (wpers-save-vpos (next-line)))

(defun wpers-previous-line () "Same as `previous-line' but adds the spaces if it's needed
for saving cursor's position in the line (column)"
  (interactive) (wpers-save-vpos (previous-line)))

;; Двигаем курсор вправо, "за пределы" конца строки
(defun wpers-right-char () "Same as `right-char' but adds the spaces if cursor at end of line (column)"
  (interactive)
  (let ((ca (char-after)))
    (if (or (null ca) (eq ca 10))
        (insert 32)
        (right-char))))

;; Двигаем курсор в конец строки и удаляем незначимые пробелы
(defun wpers-move-end-of-line () "Function `move-end-of-line' is called and then removes all trailing spaces"
  (interactive)
  (move-end-of-line nil)
  (while (eq (char-before) 32) (delete-char -1)))

;; Удаляем пробелы в конце всех строк буфера
(defun wpers-kill-final-spaces () "Deleting all trailing spaces for all lines in the buffer"
  (save-excursion
   (goto-char (point-min))
   (while (search-forward-regexp " +$" nil t) (replace-match ""))))

;; Выключаем функционал режима (возвращаемся к прежним обработчикам команд) в режиме read-only, visual-line или в режиме отметки.
(defun wpers--pre-command-hook () "Disabling functionality when buffer is read only, visual-line-mode is non-nil or marking is active"
  (if (or buffer-read-only this-command-keys-shift-translated mark-active visual-line-mode)
      (let ((fn-pair (rassoc this-command wpers-funs-alist)))
        (when fn-pair (setq this-command (car fn-pair))))))


Заключение


Приведённое решение безусловно не является идеальным и имеет ряд ограничений. Например, режим по понятным причинам не работает для read-only буферов. Лишние пробелы возможно лучше было бы удалять «по горячим следам», скажем в post-command-hook. Возможно на досуге я займусь решением этих и других проблем, однако в настоящий момент я вполне доволен… возможно не «как слон», но определённо как старый лиспер и начинающий емаксер ;)

Материалы по теме




UPD: читайте в продолжении как сделать то же самое без лишних пробелов с использованием оверлеев.

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


  1. Cheater
    27.08.2015 22:34

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

    Много лишних модификаций текста (undo в таком режиме превращается в кошмар). Не работает в readonly буферах. Удалит временные пробелы только после 4 фиксированных движений, а их десятки (isearch, Esc+<, Esc+>, прыжки Semantic и Evil...).

    Короче, если такое и делать, то это уже надо править сишный код Emacs.


    1. Wardopdem
      27.08.2015 23:25

      Удалит временные пробелы только после 4 фиксированных движений
      Не совсем так, точнее пока совсем не так ;)
      Удалит только после выхода из режима или сохранении буфера.
      Вот как раз по причине сложности отслеживания возможных действий (и их последствий), после которых надо/ненадо удалять пробелы пока сделал именно так.

      Много лишних модификаций текста (undo в таком режиме превращается в кошмар).
      Есть такое дело, можно попробовать поиграться с отключением undo на вставку пробелов, но боюсь это может привести к неожиданным (и неприятным) последствиям… в общем «будем посмотреть»…

      Короче, если такое и делать, то это уже надо править сишный код Emacs.
      Хотелось принципиально без этого обойтись, раз уж «конструктор редакторов» то будь любезен так сказать соответствовать… ;)

      Идея любопытная, но так её реализовывать нельзя, разве что с елиспом поиграться.
      Ну в основном хотелось именно «прощупать» до какой степени можно «настроить под себя». Хотя на практике при «сырцовых» файлах в пределах нескольких тысяч строк тормозов не заметил, вполне себе комфортно работается, но сама идея о замусоривании буфера в принципе конечно не сильно радует…


    1. Wardopdem
      08.09.2015 10:02

      В общем кажется получилось без правки C-кода — на оверлеях. Лежит здесь, статья (продолжение) тут.