Не бойтесь совершенства. Вам его не достичь!
В предыдущей статье, речь шла о том, как можно заставить курсор Emacs сохранять позицию в строке (столбец), при переходе к более короткой строке (грубо говоря — избавиться от «прыжков» курсора). Предложенное решение пожалуй обладало единственным достоинством — предельной простотой кода. Напомню, что для позиционирования курсора просто использовались дополнительные (лишние) пробелы.
Более основательное знакомство с Emacs Lisp и общение с откликнувшимися сведущими людьми (respect2: Иван Алексеев aka Yurii Sapfot) укрепило в мысли, что более правильное решение следует искать в направлении оверлеев. Так появилась версия №2 которую я и предлагаю уважаемым читателям.
Собственно опять таки решение очевидно (при наличии определённого багажа знаний): использовать свойство before-string оверлея нулевой длины для позиционирования курсора в нужную позицию (разумеется при установленном моноширинном шрифте).
Общая структура решения осталась прежней: реализуется minor mode (wpers-mode) в рамках которого "ремапятся" базовые команды управления курсором (next-line, previous-line, left-char, right-char, backward-delete-char-untabify, move-end-of-line, move-beginning-of-line, scroll-up и scroll-down).
В первом варианте приходилось добавлять лишние пробелы, сейчас вместо этого мы будем просто создавать в текущей позиции оверлей нулевого размера и устанавливать его свойство before-string (далее по тексту для простоты изложения будем подразумевать именно это свойство говоря об оверлее в целом) в строку, состоящую из необходимого количества пробелов (или иных символов — см. листинг).
Далее, при перемешении курсора в пределах досягаемости оверлея (влево-вправо) мы будем просто корректировать значение этого свойства, увеличивая или уменьшая строку пробелов вплоть до пустой строки — в этом случае оверлей просто удаляется. Если же курсор выходит за пределы «зоны влияния» оверлея (вверх-вниз), то мы просто его удаляем и, при необходимости (переход вверх-вниз на более короткую строку), создаём новый. Наконец, при вводе какого-либо символа (включая тот же пробел) после ряда «оверлейных пробелов» мы удаляем оверлей, «легализовав» при этом все накопленные пробелы в реальные пробелы внутри буфера.
Полный код второй версии пакета можно получить с GitHub, здесь я вкратце пройдусь по ключевым фрагментам, не акцентирую внимания на мелочах (есть doc-string да и код достаточно компактен и прозрачен).
Итак, начнём с набора утилит, для работы с оверлеем:
Теперь заёмёмся позиционированием курсора в строке:
Далее, определим набор функций для организации перехвата команд, влияющих на позицию курсора:
Теперь определим ключевые «перехватчики» вызываемые до и после каждой команды:
Пропустим «кухню» (аксессоры) и перейдём сразу к изменениям (дополнениям) в публичном интерфейсе модуля:
Допускаю, что и это решение не лишено недостатков (некоторые я уже вижу), однако в целом прогресс думаю имеет место быть, как в плане практических результатов так и в плане продвижения по путипостижения Дао изучения Emacs ;) Как и прежде (доброжелательные и конструктивные) комментарии ожидаются и приветствуются.
Помимо ранее приведённых гугло-очевидных источников, некоторую полезную информацию почерпнул здесь.
Сальвадор Дали
Взгляв в прошлое
В предыдущей статье, речь шла о том, как можно заставить курсор Emacs сохранять позицию в строке (столбец), при переходе к более короткой строке (грубо говоря — избавиться от «прыжков» курсора). Предложенное решение пожалуй обладало единственным достоинством — предельной простотой кода. Напомню, что для позиционирования курсора просто использовались дополнительные (лишние) пробелы.
Более основательное знакомство с Emacs Lisp и общение с откликнувшимися сведущими людьми (respect2: Иван Алексеев aka Yurii Sapfot) укрепило в мысли, что более правильное решение следует искать в направлении оверлеев. Так появилась версия №2 которую я и предлагаю уважаемым читателям.
Попытка №2
Собственно опять таки решение очевидно (при наличии определённого багажа знаний): использовать свойство before-string оверлея нулевой длины для позиционирования курсора в нужную позицию (разумеется при установленном моноширинном шрифте).
Общая структура решения осталась прежней: реализуется minor mode (wpers-mode) в рамках которого "ремапятся" базовые команды управления курсором (next-line, previous-line, left-char, right-char, backward-delete-char-untabify, move-end-of-line, move-beginning-of-line, scroll-up и scroll-down).
В первом варианте приходилось добавлять лишние пробелы, сейчас вместо этого мы будем просто создавать в текущей позиции оверлей нулевого размера и устанавливать его свойство before-string (далее по тексту для простоты изложения будем подразумевать именно это свойство говоря об оверлее в целом) в строку, состоящую из необходимого количества пробелов (или иных символов — см. листинг).
Далее, при перемешении курсора в пределах досягаемости оверлея (влево-вправо) мы будем просто корректировать значение этого свойства, увеличивая или уменьшая строку пробелов вплоть до пустой строки — в этом случае оверлей просто удаляется. Если же курсор выходит за пределы «зоны влияния» оверлея (вверх-вниз), то мы просто его удаляем и, при необходимости (переход вверх-вниз на более короткую строку), создаём новый. Наконец, при вводе какого-либо символа (включая тот же пробел) после ряда «оверлейных пробелов» мы удаляем оверлей, «легализовав» при этом все накопленные пробелы в реальные пробелы внутри буфера.
Полный код второй версии пакета можно получить с GitHub, здесь я вкратце пройдусь по ключевым фрагментам, не акцентирую внимания на мелочах (есть doc-string да и код достаточно компактен и прозрачен).
Итак, начнём с набора утилит, для работы с оверлеем:
;; Разукрашиваем оверлей при активном режиме выделения текущей строки
(defun wpers--ovr-propz-txt (txt)
(if (or hl-line-mode global-hl-line-mode)
(propertize txt 'face (list :background (face-attribute 'highlight :background)))
txt))
;; Создаём оверлей 0-й длины в текущей позиции, не забыв уничтожить прежний (если таковой имелся)
(defun wpers--ovr-make (&optional str)
(wpers--ovr-kill)
(setq wpers--overlay (make-overlay (point) (point)))
(overlay-put wpers--overlay 'wpers t)
(if str (overlay-put wpers--overlay 'before-string (wpers--ovr-propz-txt str))))
;; Проверка наличия оверлея в текущей позиции буфера
(defun wpers--ovr-at-point-p ()
(eq (point) (overlay-start wpers--overlay)))
;; Проверка наличия текста в строке после позиции оверлея
(defun wpers--ovr-txt-after-p ()
(when wpers--overlay
(let ((ch (char-after (overlay-start wpers--overlay))))
(and ch (not (eq ch 10))))))
;; "Легализация" оверлейных пробелов в буферные
(defun wpers--ovr-to-spcs ()
(let ((ovr-size (when (wpers--ovr-at-point-p) (length (wpers--ovr-get)))))
(save-excursion
(goto-char ov-pos)
(insert (make-string (length (wpers--ovr-get)) 32)))
(when ovr-size (right-char ovr-size))))
;; Уничтожение оверлея с "легализацией" пробелов при необходимости
(defun wpers--ovr-kill ()
(when wpers--overlay
(let* ((ov-pos (overlay-start wpers--overlay))
(ch (char-after ov-pos)))
(when (and ch (not (eq ch 10))) (wpers--ovr-to-spcs)))
(delete-overlay wpers--overlay)
(setq wpers--overlay nil)))
;; Уничтожаем оверлеи во всех буферах кроме текущего
(defun wpers--clean-up-ovrs ()
(mapc #'(lambda (b)
(when (and (local-variable-p 'wpers-mode b)
(buffer-local-value 'wpers-mode b)
(buffer-local-value 'wpers--overlay b)
(not (eq b (current-buffer))))
(wpers--ovr-kill b)))
(buffer-list)))
;; Чтение свойства before-string
(defun wpers--ovr-get ()
(overlay-get wpers--overlay 'before-string))
;; Установка свойства before-string с "раскраской текста" и возможностью выполнения
;; каких-либо операций над текущим значением этого свойства, связываемым с переменной "_"
(defmacro wpers--ovr-put (val)
`(let ((_ (wpers--ovr-get)))
(overlay-put wpers--overlay 'before-string (wpers--ovr-propz-txt ,val))))
Теперь заёмёмся позиционированием курсора в строке:
;; Текущая позиция курсора в строке (столбец) с учётом возможного наличия оверлея
(defun wpers--current-column ()
(let ((res (current-column)))
(if (and wpers--overlay (wpers--ovr-at-point-p))
(+ res (length (wpers--ovr-get)))
res)))
;; Позиционирование курсора в нужную позицию внутри строки (на экране - не в буфере!) с использованием оверлея
(defun wpers--move-to-column (col)
(move-to-column col)
(let* ((last-column (- (line-end-position) (line-beginning-position)))
(spcs-needed (- col last-column)))
(when (plusp spcs-needed)
(wpers--ovr-make (make-string spcs-needed wpers--pspace)))))
;; Выполнение произвольного выражения с сохранением позиции курсора в строке (столбца)
(defmacro wpers--save-vpos (form)
(let ((old-col (make-symbol "old-col")))
`(let ((,old-col (wpers--current-column))) ,form (wpers--move-to-column ,old-col))))
Далее, определим набор функций для организации перехвата команд, влияющих на позицию курсора:
;; Базовая функция создания "обёрток" для команд управления курсором
(defun wpers--remap (key body &optional params)
(let ((old (wpers--key-handler key)) ;; запомнили текущий обработчик
(fun `(lambda ,params ;; новый обработчик
"WPERS handler: perform operation with saving current cursor's position in the line (column)."
,@body)))
(when old (add-to-list 'wpers--funs-alist (cons old fun))) ;; зафиксировали связь старый-новый обработчик
(define-key wpers--mode-map key fun))) ;; установили новый обработчик в keymap режима
;; "Обёртка" для команд перемещающих курсор по вертикали
(defun wpers--remap-vert (command &optional key)
(wpers--remap (wpers--mk-key command key)
`((interactive)(wpers--save-vpos (call-interactively ',command)))))
;; "Обёртка" для "идёт налево"
(defun wpers--remap-left (command &optional key)
(let ((key (wpers--mk-key command key))
(expr `(call-interactively ',command)))
(wpers--remap key
`((interactive)
(if wpers--overlay
(if (and (wpers--ovr-at-point-p) (wpers--at-end (point)))
(if (plusp (length (wpers--ovr-get)))
(wpers--ovr-put (substring _ 1))
(wpers--ovr-kill) ,expr)
(wpers--ovr-kill) ,expr)
,expr)))))
;; "Обёртка" для "идёт направо"
(defun wpers--remap-right (command &optional key)
(let ((key (wpers--mk-key command key))
(expr `(call-interactively ',command)))
(wpers--remap key
`((interactive)
(if (wpers--at-end (point))
(if (null wpers--overlay)
(wpers--ovr-make (string wpers-pspace))
(if (wpers--ovr-at-point-p)
(wpers--ovr-put (concat _ (string wpers-pspace)))
(wpers--ovr-kill) (wpers--ovr-make (string wpers-pspace))))
(wpers--ovr-kill) ,expr)))))
;; Позаботимся о "братьях меньших"
(defun wpers--remap-mouse (command)
(wpers--remap (vector 'remap command) `(
(interactive "e")
(funcall ',command event)
(let ((col (car (posn-col-row (cadr event)))))
(wpers--move-to-column col))) '(event)))
Теперь определим ключевые «перехватчики» вызываемые до и после каждой команды:
;; Выключаем режим при активной (активации) отметке, visual-line-mode или "размазанных" строках (truncate-lines равна nil)
;; NB: read-only теперь не помеха работе режима
(defun wpers--pre-command-hook ()
(if (member this-command wpers-ovr-killing-funs)
(wpers--ovr-kill)
(if (or this-command-keys-shift-translated mark-active visual-line-mode (null truncate-lines))
(let ((fn-pair (rassoc this-command wpers--funs-alist)))
(when fn-pair (setq this-command (car fn-pair)))))))
;; Удаляем оверлей если верно одно из условий:
;; - курсор не находится в позиции оверлея
;; - имеется текст в буфере после позиции оверлея, но до конца строки
(defun wpers--post-command-hook ()
(when (and wpers--overlay
(or (not (wpers--ovr-at-point-p))
(wpers--ovr-txt-after-p)))
(wpers--ovr-kill)))
;; Уничтожаем оверлеи режима во всех буферах кроме текущего
(add-hook 'post-command-hook 'wpers--clean-up-ovrs)
Пропустим «кухню» (аксессоры) и перейдём сразу к изменениям (дополнениям) в публичном интерфейсе модуля:
;; Это свойство определяет способ отображения оверлейных пробелов:
;; nil - невидимы
;; t - отображаются в виде маленькой точки по центру символа (символ с кодом 183)
;; иное число - код символа, который будет отображаться
(defcustom wpers-pspace 32
:type `(choice (const :tag "Standard visible" t)
(const :tag "Invisible" nil)
(character :tag "Custom visible"))
:get 'wpers--get-pspace
:set 'wpers--set-pspace
:set-after '(wpers--pspace-def))
;; Функция для включения/выключения оверлейных пробелов - альтернатива custom-ного доступа к wpers-pspace
(defun wpers-overlay-visible (val) "Toggle overlay visibility if VAL is nil, swtich on if t else set to VAL"
(interactive "P")
(wpers--set-pspace nil
(cond
((null val) t)
((member val '(- (4))) nil)
(t val))))
;; Список команд, после выполнения которых без всяких условий overlay must die!
(defcustom wpers-ovr-killing-funs '(undo move-end-of-line move-beginning-of-line)
"Functions killing overlay"
:type '(repeat function))
;; Ассоциативный список каждая пара которого имеет вид (handler . commands)
;; где handler - одна из вышеописанных функций wpers--remap-...
;; commands - список каждый элемент которого либо непосредственно команда (символ),
;; либо список вида (command key) - в этом случае key это строка передаваемая функции kbd
(defcustom wpers-remaps
'((wpers--remap-vert next-line previous-line scroll-up-command scroll-down-command
(scroll-down-command "<prior>") (scroll-up-command "<next>")) ; for CUA mode
(wpers--remap-left left-char backward-char backward-delete-char backward-delete-char-untabify)
(wpers--remap-right right-char forward-char)
(wpers--remap-mouse mouse-set-point))
:options '(wpers--remap-vert wpers--remap-left wpers--remap-right wpers--remap-mouse)
:type '(alist :key-type symbol :value-type (repeat (choice function (list symbol string))))
:set 'wpers--set-remaps)
Резюме
Допускаю, что и это решение не лишено недостатков (некоторые я уже вижу), однако в целом прогресс думаю имеет место быть, как в плане практических результатов так и в плане продвижения по пути
Помимо ранее приведённых гугло-очевидных источников, некоторую полезную информацию почерпнул здесь.