К чему эти прыжки?
Вообще-то я хотел написать небольшую заметку «о некоторых особенностях работы с макросами в Clojure». Но попутно решил наконец более основательно ознакомиться с Emacs.
Я конечно не совсем ровестник Lisp, однако знакомы мы вот уже… дцать лет и потенциал этого замечательного языка (даже скорее философии) вполне себе представляю и в теории и на практике. Было дело писал и свои реализации (скорее для лучшего понимания механизмов работы интерпретатора Lisp нежели для практического использования). Однако, Emacs практически не использовал т.к. в стародавние времена достаточно плотной работы с Lisp вполне обходился встроенным редактором моей версии (muLisp, редактор конечно же тоже был написан на нём самом). Потом приходилось работать с «более другими» инструментами, а последние годы и вовсе в иной сфере. Сейчас вот появилось немного времени «для души»…
Собственно «погружение» в Emacs прошло вполне комфортно — хотя я (почему-то всё ещё) и не юниксоид, но к консольным командам и вообще работе с клавиатурой отношусь с пониманием. Настройка управления и джентльменского набора «плагинов» также не вызвала проблем. С прикручиванием SBCL, Clojure и Scala пришлось немного повозиться, но всему виной было несоответствие версий и/или их (версий) врождённые проблемы.
Однако синдром «прыгающего курсора» (перемещение к концу строки при переходе к следующей/предыдущей строке в случае, если она короче текущей) вызывает лёгкую идиосинкразию. Если бы дело шло не о Emacs, то скорее всего пришлось бы смириться и искать «концептуальность» в таком подходе, как это часто делается при невозможности решения проблем. Но, поскольку мы имеем дело с конструктором редакторов, то проблема была трактована как вызов (как сейчас стало модно говорить).
Итак оставим в стороне сам вопрос необходимости «дрессировки» курсора — лично для меня это «50 на 50» удобство и простохорошая физика неплохое упражнение для ознакомления с концепцией программирования Emacs. Возможно кому-то тоже будет интересно и полезно, по крйней мере «на вскидку» готовых решений я не нашёл (только вопросы «а как сделать?..») — допускаю, что не сильно искал по причине см. выше.
Сразу оговорюсь, что для тех, кто хорошо знаком с Emacs Lisp это задачка на пол часа, так, что статья адресована прежде всего тем, кто начинает знакомство с практическими возможностями программирования Emacs. Стиль изложения подразумевает знание основ Emacs Lisp и базовых понятий Emacs.
В свою очередь от экспертов хотельсь бы услышать комментарии по поводу альтернативных решений которых я возможно не увидел по причине пока достаточно поверхностного знакомства с архитектурой Emacs.
Собственно, моё решение достаточно очевидно:
Была идея отслеживать именно те пробелы, которые добавляются для перемещения курсора в нужную позицию, однако это существенно усложнило бы решение не принося какой-то особой пользы поэтому будем удалять просто все пробелы в конце строк (строго говоря со следующего за непробельным символом до конца строки). Что касается момента удаления таких побелов, то, также для упрощения, ограничимся событиями выключения режима и сохранения буфера.
Итак, приступим к созданию minor mode который будет называться скажем wpers (далее по тексту просто режим). Полный текст пакета с краткой инструкцией по установке вы можете получить с GitHub. Здесь я подробно прокомментирую весь код.
Концептуально, всё, что нам необходимо — это перехватить команды next-line, previous-line, right-char и move-end-of-line. Первые три просто должны добавлять пробелы, при необходимости, последняя будет автоматически удалять «лишние» пробелы в конец строки. Перехват осуществляется стандартными средствами remap в рамках key-map-а локального для режима.
По возможности вынесем всё что можно из кода в константы:
Далее, определим сам режим:
Теперь собственно нехитрая «кухня» функционала режима:
Приведённое решение безусловно не является идеальным и имеет ряд ограничений. Например, режим по понятным причинам не работает для read-only буферов. Лишние пробелы возможно лучше было бы удалять «по горячим следам», скажем в post-command-hook. Возможно на досуге я займусь решением этих и других проблем, однако в настоящий момент я вполне доволен… возможно не «как слон», но определённо как старый лиспер и начинающий емаксер ;)
UPD: читайте в продолжении как сделать то же самое без лишних пробелов с использованием оверлеев.
Остап Бендер
Вступление
Вообще-то я хотел написать небольшую заметку «о некоторых особенностях работы с макросами в Clojure». Но попутно решил наконец более основательно ознакомиться с Emacs.
Я конечно не совсем ровестник Lisp, однако знакомы мы вот уже… дцать лет и потенциал этого замечательного языка (даже скорее философии) вполне себе представляю и в теории и на практике. Было дело писал и свои реализации (скорее для лучшего понимания механизмов работы интерпретатора Lisp нежели для практического использования). Однако, Emacs практически не использовал т.к. в стародавние времена достаточно плотной работы с Lisp вполне обходился встроенным редактором моей версии (muLisp, редактор конечно же тоже был написан на нём самом). Потом приходилось работать с «более другими» инструментами, а последние годы и вовсе в иной сфере. Сейчас вот появилось немного времени «для души»…
Собственно «погружение» в Emacs прошло вполне комфортно — хотя я (почему-то всё ещё) и не юниксоид, но к консольным командам и вообще работе с клавиатурой отношусь с пониманием. Настройка управления и джентльменского набора «плагинов» также не вызвала проблем. С прикручиванием SBCL, Clojure и Scala пришлось немного повозиться, но всему виной было несоответствие версий и/или их (версий) врождённые проблемы.
Однако синдром «прыгающего курсора» (перемещение к концу строки при переходе к следующей/предыдущей строке в случае, если она короче текущей) вызывает лёгкую идиосинкразию. Если бы дело шло не о Emacs, то скорее всего пришлось бы смириться и искать «концептуальность» в таком подходе, как это часто делается при невозможности решения проблем. Но, поскольку мы имеем дело с конструктором редакторов, то проблема была трактована как вызов (как сейчас стало модно говорить).
Итак оставим в стороне сам вопрос необходимости «дрессировки» курсора — лично для меня это «50 на 50» удобство и просто
Целевая аудитория
Сразу оговорюсь, что для тех, кто хорошо знаком с 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: читайте в продолжении как сделать то же самое без лишних пробелов с использованием оверлеев.
Cheater
Идея любопытная, но так её реализовывать нельзя, разве что с елиспом поиграться.
Много лишних модификаций текста (undo в таком режиме превращается в кошмар). Не работает в readonly буферах. Удалит временные пробелы только после 4 фиксированных движений, а их десятки (isearch, Esc+<, Esc+>, прыжки Semantic и Evil...).
Короче, если такое и делать, то это уже надо править сишный код Emacs.
Wardopdem
Удалит только после выхода из режима или сохранении буфера.
Вот как раз по причине сложности отслеживания возможных действий (и их последствий), после которых надо/ненадо удалять пробелы пока сделал именно так.
Есть такое дело, можно попробовать поиграться с отключением undo на вставку пробелов, но боюсь это может привести к неожиданным (и неприятным) последствиям… в общем «будем посмотреть»…
Хотелось принципиально без этого обойтись, раз уж «конструктор редакторов» то будь любезен так сказать соответствовать… ;)
Ну в основном хотелось именно «прощупать» до какой степени можно «настроить под себя». Хотя на практике при «сырцовых» файлах в пределах нескольких тысяч строк тормозов не заметил, вполне себе комфортно работается, но сама идея о замусоривании буфера в принципе конечно не сильно радует…
Wardopdem
В общем кажется получилось без правки C-кода — на оверлеях. Лежит здесь, статья (продолжение) тут.