LINUX.ORG.RU

GNU Emacs: isearch-backward-kill-word или что-то типа того

 , ,


0

2

Зело меня огорчает, что в ГНУ Емаксе, а точнее в интерактивном поиске (isearch’е) нет команды вида «стереть последнее слово» — DEL стирает последнюю букву в строке поиска, M-DEL же пролетает насквозь в буфер — дико бесит!

Наверное, ее можно навелосипедировать самому, но ведь наверняка кто-нибудь уже написал какой-нибудь isearch-backward-kill-word, а я просто как-то не так ищу, а потому не могу найти, верно?

★★★★★

Варианты:

  • просто использовать phi-search вместо isearch.
  • попробуй поиграться со значениями search-quit-options и isearch-pre-command-hook.
theNamelessOne ★★★★★
()

но ведь наверняка кто-нибудь уже написал какой-нибудь isearch-backward-kill-word

Вот только что написал.

(defun isearch-backward-kill-word ()
  (interactive)
  (if (null (cdr isearch-cmds))
      (ding)
    (let* ((current (string-reverse (isearch--state-string (car isearch-cmds))))
           (match (string-match "[[:word:]][^[:word:]]" current)))
      (while (and (not (null (cdr isearch-cmds)))
                  (or (null match)
                      (>= match 0)))
        (setf isearch-cmds (cdr isearch-cmds)
              match (if (null match) match (1- match))))
      (isearch--set-state (car isearch-cmds))))
  (isearch-update))

Добавь binding к isearch-mode-map:

(define-key isearch-mode-map (kbd "M-DEL") #'isearch-backward-kill-word)
theNamelessOne ★★★★★
()
Последнее исправление: theNamelessOne (всего исправлений: 1)
Ответ на: комментарий от theNamelessOne

просто использовать phi-search вместо isearch

Занятно, спасибо, при случае посмотрю поближе, однако одна особенность (хотя и более чем вероятно, поправимая) настраивает против этого варианта: C-g сразу отменяет поиск вовсе, но не стирает ненайденную часть.

поиграться со значениями search-quit-options и isearch-pre-command-hook

Вероятно, я слишком тупой, чтоб понять, как это может помочь.

Zmicier ★★★★★
() автор топика
Ответ на: комментарий от theNamelessOne

Вот только что написал.

Выглядит внушительно. И, разумеется, работает. (Разве что на современном Емаксе string-reverse называется просто reverse.) Низкий поклон.

А можете еще об’яснить, почему надо с такими сложностями оперировать именно isearch-cmds, а не isearch-string?

Zmicier ★★★★★
() автор топика
Ответ на: комментарий от Zmicier

Вероятно, я слишком тупой, чтоб понять, как это может помочь.

Никак, это было ошибочное предположение. Курение исходников isearch показало, что вряд ли получится научить isearch работать со стандартными функциями типа backward-kill-word.

Поэтому тут рабочий вариант.

theNamelessOne ★★★★★
()
Ответ на: комментарий от theNamelessOne

А можете еще об’яснить, почему надо с такими сложностями оперировать именно isearch-cmds, а не isearch-string?

Потому что isearch-string – это только строка поиска, которая отображается в минибуфере. Само состояние хранится в стеке isearch-cmds. Каждый элемент этого стека – состояние поиска в определённый момент: текущая введённая строка, позиция совпадения в родительском буфере, успех и пр. Это позволяет реализовать именно инкрементальный поиск с возможностью возврата к предыдущим состояниям. Команды, введённые в минибуфере (включая self-insert-command), модифицируют этот стек: при вводе символа в стек добавляется элемент-состояние, при удалении символа – удаляется элемент. После каждой команды на основе этого стека устанавливается новое значение isearch-string и кучи других переменных с помощью функции isearch--set-state, затем вызывается isearch-update для применения изменений. Можно сказать, что значение isearch-string на любом шаге — это значение поля isearch-string элемента в голове стека.

с такими сложностями

На самомо деле, тут ничего сложного нет. Как и в функции backward-kill-word, сначала определяется, сколько символов от текущей позиции до границы слова, просто вместо удаления N символов мы дропаем N элементов стека isearch-cmds.

theNamelessOne ★★★★★
()
Последнее исправление: theNamelessOne (всего исправлений: 1)
Ответ на: комментарий от Zmicier

C-g сразу отменяет поиск вовсе, но не стирает ненайденную часть.

Хм, а я что-то такой фичей даже не пользовался не разу. Можешь открыть issue или сам попробовать запилить.

theNamelessOne ★★★★★
()
Последнее исправление: theNamelessOne (всего исправлений: 1)
Ответ на: комментарий от theNamelessOne

Благодарю, кажется, немного понятнее, но не вполне.

Потому что isearch-string – это только строка поиска, которая отображается в минибуфере
Это позволяет реализовать именно инкрементальный поиск с возможностью возврата к предыдущим состояниям.

А к каким состояниям должна возвращать команда с семантикой «стереть слово»?

DEL по-умолчанию (будучи привязанным к isearch-delete-char, который опосредованно оперирует именно isearch-cmds) отменяет еще и эффект C-s / C-r, то есть возвращает курсор назад к прошлой находке / вперед к следующей. То есть чтобы стереть o после C-s foo C-s C-s, DEL нужно жать трижды.

Это, к слову, мне не нравилось куда более, чем сабж, но это-то легко исправлялось из коробки — благо есть isearch-del-char, который оперирует уже isearch-string и всегда удаляет последний символ.

--приложение--

(defun isearch-pop-state ()
  (setq isearch-cmds (cdr isearch-cmds))
  (isearch--set-state (car isearch-cmds)))

(defun isearch-delete-char ()
  "Discard last input item and move point back.
Last input means the last character or the last isearch command
that added or deleted characters from the search string,
moved point, toggled regexp mode or case-sensitivity, etc.
If no previous match was done, just beep."
  (interactive)
  (if (null (cdr isearch-cmds))
      (ding)
    (isearch-pop-state))
  (isearch-update))

(defun isearch-del-char (&optional arg)
  "Delete character from end of search string and search again.
Unlike `isearch-delete-char', it only deletes the last character,
but doesn't cancel the effect of other isearch command.
If search string is empty, just beep."
  (interactive "p")
  (if (= 0 (length isearch-string))
      (ding)
    (setq isearch-string (substring isearch-string 0
				    (- (min (or arg 1)
					    (length isearch-string))))
          isearch-message (mapconcat 'isearch-text-char-description
                                     isearch-string "")))
  ;; Do the following before moving point.
  (funcall (or isearch-message-function #'isearch-message) nil t)
  ;; Use the isearch-other-end as new starting point to be able
  ;; to find the remaining part of the search string again.
  ;; This is like what `isearch-search-and-update' does,
  ;; but currently it doesn't support deletion of characters
  ;; for the case where unsuccessful search may become successful
  ;; by deletion of characters.
  (if isearch-other-end (goto-char isearch-other-end))
  (isearch-search)
  (isearch-push-state)
  (isearch-update))
Zmicier ★★★★★
() автор топика
Последнее исправление: Zmicier (всего исправлений: 1)
Ответ на: комментарий от Zmicier

А к каким состояниям должна возвращать команда с семантикой «стереть слово»?

К первому (начиная с головы стека) состоянию, в котором строка поиска будет равна строке поиска без этого слова.

Вот простой пример. Пусть у нас следующее содержимое буфера (¨|¨ — это не введённый символ, а позиция курсора):

puts "Hello|, foo-bar"

Запустим isearch-forward, начальное содержимое стека (здесь и далее голова стека находится сверху):

([cl-struct-isearch--state "" "" 12 t t nil nil nil nil 12 t nil])

Нас интересует первый и третий параметры: строка поиска на данный момент и соответствующая позиция курсора в родительском буфере.

Введём в минибуфере foo-bar, позиция в родительском буфере puts "Hello, foo-bar|", содержимое стека с номерами состояний:

([cl-struct-isearch--state "foo-bar" "foo-bar" 21 21 t 14 nil nil nil 12 t nil] ; 8
 [cl-struct-isearch--state "foo-ba" "foo-ba" 20 20 t 14 nil nil nil 12 t nil]   ; 7
 [cl-struct-isearch--state "foo-b" "foo-b" 19 19 t 14 nil nil nil 12 t nil]     ; 6
 [cl-struct-isearch--state "foo-" "foo-" 18 18 t 14 nil nil nil 12 t nil]       ; 5
 [cl-struct-isearch--state "foo" "foo" 17 17 t 14 nil nil nil 12 t nil]         ; 4
 [cl-struct-isearch--state "fo" "fo" 16 16 t 14 nil nil nil 12 t nil]           ; 3
 [cl-struct-isearch--state "f" "f" 15 15 t 14 nil nil nil 12 t nil]             ; 2
 [cl-struct-isearch--state "" "" 12 t t nil nil nil nil 12 t nil])              ; 1

Команда ¨стереть слово¨ должна удалить слово ¨bar¨ и вернуть нас к (первому с головы стека) состоянию, соответствующему строке поиска foo- и позиции родительского буфера puts "Hello, foo-|bar" (состояние номер 5).

theNamelessOne ★★★★★
()
Ответ на: комментарий от theNamelessOne

Я, кстати, обнаружил баг в isearch-backward-kill-word, который появляется, если использовать в isearch команды повторного поиска (C-s, C-r, т.е. isearch-repeat-forward и isearch-repeat-backward).

Пример. Содержимое буфера — test| foo-bar foo-bar foo-bar, ¨|¨ — позиция курсора. Если ввести C-s foo- C-s bar C-s M-DEL (первый C-s запускает поиск), то получим такую позицию курсора в родительском буфере: test foo-bar foo-b|ar foo-bar.

Баг легко исправить, но с учётом твоего замечания о isearch-delete-char/isearch-del-char это можно сделать двумя способами. Рассмотрим промежуточные состояния:

test| foo-|bar foo-|bar| foo-bar|
    ^     ^        ^   ^        ^
    |     |        |   |        |
    |     |       /   /        /
    |     |      /   /   -----/  
     \    |     /   /   /
      \   |    |   |   /
     --- ---  --- --- ---
     C-s foo- C-s bar C-s
         (1)  (2)     (3)

Из состояния (3) M-DEL может вернуть в состояние (1) или (2).

  • команда для перевода в состояние (2):
    (defun drop-until-backwards (re string)
      (let ((match (string-match re (string-reverse string))))
        (if match (substring string 0 (- (length string) match 1)) "")))
    
    (defun isearch-backward-kill-word ()
      (interactive)
      (if (null (cdr isearch-cmds))
          (ding)
        (let* ((current-state (car isearch-cmds))
               (current-string (isearch--state-string current-state))
               (new-string (drop-until-backwards "[[:word:]][^[:word:]]" current-string)))
          (while (not (string-equal current-string new-string))
            (setf isearch-cmds (cdr isearch-cmds)
                  current-state (car isearch-cmds)
                  current-string (isearch--state-string current-state)))
          (isearch--set-state (car isearch-cmds))))
      (isearch-update))
    
  • команда для перевода в состояние (1):
    (defun isearch-backward-kill-word-greedy ()
      (interactive)
      (isearch-backward-kill-word)
      (let* ((current-state (car isearch-cmds))
             (tail (cdr isearch-cmds))
             (string (isearch--state-string current-state)))
        (while (and tail (string-equal string (isearch--state-string (car tail))))
          (setf current-state (car tail)
                tail (cdr tail)))
        (isearch--set-state current-state)
        (isearch-update)))
    
theNamelessOne ★★★★★
()
Последнее исправление: theNamelessOne (всего исправлений: 2)
Ответ на: комментарий от theNamelessOne

Еще раз здравствуйте.

Извините, что долго не подходил к вопросу, сейчас вернусь, но сперва поинтересуюсь – чем нарисована схемка? artist-mode’ом?

Баг легко исправить, но с учётом твоего замечания о isearch-delete-char/isearch-del-char это можно сделать двумя способами.

Дело как раз в том, что тремя. И многократный isearch-del-char соответствует как раз неуказанному вами третьему — (4):

test| foo-|bar foo-|bar| foo-|bar|
    ^     ^        ^   ^     ^   ^
    |     |        |   |     |   |
    |     |       /   /  /---+---/
    |     |      /   /  /   /
     \    |     /   /  /   |
      \   |    |   |   |   |
     --- ---  --- --- --- ---
     C-s foo- C-s bar C-s M-DEL
         (1)  (2)     (3) (4)

На основе isearch-del-char у меня вышло что-то такое:

(defun string-remove-suffix-regex (suffix string)
  (let ((match (string-match (concat suffix "$") string)))
    (if match
        (substring string 0 match)
      string)))

(defun string-delete-last-word (string)
  (string-remove-suffix-regex "[[:word:]]*[^[:word:]]*" string))

(defun isearch-del-word ()
  "Delete word from end of search string and search again."
  ;; This function is mostly borrowed from `isearch-del-char'.
  (interactive)
  (if (= 0 (length isearch-string))
      (ding)
    (setq isearch-string (string-delete-last-word isearch-string)
          isearch-message (mapconcat 'isearch-text-char-description
                                     isearch-string "")))
  ;; Do the following before moving point.
  (funcall (or isearch-message-function #'isearch-message) nil t)
  ;; Use the isearch-other-end as new starting point to be able
  ;; to find the remaining part of the search string again.
  ;; This is like what `isearch-search-and-update' does,
  ;; but currently it doesn't support deletion of characters
  ;; for the case where unsuccessful search may become successful
  ;; by deletion of characters.
  (if isearch-other-end (goto-char isearch-other-end))
  (isearch-search)
  (isearch-push-state)
  (isearch-update))

(define-key isearch-mode-map (kbd "M-DEL") #'isearch-del-word)

И оно, кажется, даже работает.

Что я из всего этого вынес, так это то, что работа со строками в Емаксе, в отличие от работы с текстом в буфере, развита на зачаточном уровне.

Спасибо за науку.

Zmicier ★★★★★
() автор топика
Ответ на: комментарий от Zmicier

Извините, что долго не подходил к вопросу, сейчас вернусь, но сперва поинтересуюсь – чем нарисована схемка? artist-mode’ом?

В данном случае — вручную.

Дело как раз в том, что тремя. И многократный isearch-del-char соответствует как раз неуказанному вами третьему — (4):

Я этот вариант не рассматривал, так как он не основан на предыдущих состояниях. Т.е. если рассматривать операцию удаления символа как отмену одной из предыдущих операций ввода символа, тогда удаление слова — это отмена последовательных операций ввода символа; при этом мы должны вернуться в одно из предыдущих состояний, а состояния, соответствующего варианту (4), у нас не было. Это логика, которой я руководствовался, если тебе удобнее по-другому — ничего страшного.

Что я из всего этого вынес, так это то, что работа со строками в Емаксе, в отличие от работы с текстом в буфере, развита на зачаточном уровне.

Ну да, основной упор делается на работу с текстом в буфере.

theNamelessOne ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.