Emacs: дрессируем курсор

от автора

К чему эти прыжки?

                 Остап Бендер

Вступление

Вообще-то я хотел написать небольшую заметку «о некоторых особенностях работы с макросами в 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 (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. Возможно на досуге я займусь решением этих и других проблем, однако в настоящий момент я вполне доволен… возможно не «как слон», но определённо как старый лиспер и начинающий емаксер 😉

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

ссылка на оригинал статьи http://habrahabr.ru/post/265531/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *