К чему эти прыжки?
Остап Бендер
Вступление
Вообще-то я хотел написать небольшую заметку «о некоторых особенностях работы с макросами в 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/
Добавить комментарий