index rss mastodon twitter github linkedin email
Álvaro Ramírez
sponsor

Álvaro Ramírez

25 May 2023 Deleting from Emacs sequence vars

Adding hooks and setting variables is core to customizing Emacs. Take a major mode like emacs-lisp-mode as an example. To customize its behaviour, one may add a hook function to emacs-lisp-mode-hook, or if you're a little lazy while experimenting, you may even use a lambda.

(add-hook 'emacs-lisp-mode-hook
          #'my/emacs-lisp-mode-config)

(add-hook 'emacs-lisp-mode-hook
          (lambda ()
            (message "I woz ere")))

emacs-lisp-mode-hook's content would subsequently look as follows:

'(my/emacs-lisp-mode-config
  (lambda nil
    (message "I woz ere"))
  ert--activate-font-lock-keywords
  easy-escape-minor-mode
  lisp-extra-font-lock-global-mode)

Maybe my/emacs-lisp-mode-config didn't work out for us and we'd like to remove it. We can use remove-hook for that and evaluate something like:

(remove-hook 'emacs-lisp-mode-hook #'my/emacs-lisp-mode-config)

The lambda can be removed too, but you ought to be careful in using the same lambda body.

(remove-hook 'emacs-lisp-mode-hook
             (lambda ()
               (message "I woz tere")))

There are other ways to remove the lambdas, but we're digressing here… We typically have to write these throwaway snippets to undo our experiments. What if we just had a handy helper always available to remove items from sequences (edit: we do, remove-hook is already interactive, see Update 2 below)? After all, hooks are just lists (sequences).

removed-lambda.gif

While the interactive command can likely be simplified further, I tried to optimize for ergonomic usage. For example, completing-read gives us a way narrow down whichever variable we'd like to modify as well as the item we'd like to remove. seqp is also handy, as we filter out noise by automatically removing any variable that's not a sequence.

(defun ar/remove-from-list-variable ()
  (interactive)
  (let* ((var (intern
               (completing-read "From variable: "
                                (let (symbols)
                                  (mapatoms
                                   (lambda (sym)
                                     (when (and (boundp sym)
                                                (seqp (symbol-value sym)))
                                       (push sym symbols))))
                                  symbols) nil t)))
         (values (mapcar (lambda (item)
                           (setq item (prin1-to-string item))
                           (concat (truncate-string-to-width
                                    (nth 0 (split-string item "\n"))
                                    (window-body-width))
                                   (propertize item 'invisible t)))
                         (symbol-value var)))
         (index (progn
                  (when (seq-empty-p values) (error "Already empty"))
                  (seq-position values (completing-read "Delete: " values nil t)))))
    (unless index (error "Eeek. Something's up."))
    (set var (append (seq-take (symbol-value var) index)
                     (seq-drop (symbol-value var) (1+ index))))
    (message "Deleted: %s" (truncate-string-to-width
                            (seq-elt values index)
                            (- (window-body-width) 9)))))

Hooks are just an example of lists we can delete from. I recently used the same command on display-buffer-alist.

alist.gif

While this has been a fun exercise, I can't help but think that I'm likely re-inventing the wheel here. Is there something already built-in that I'm missing?

Update 1

alphapapa suggested some generalizations that would provide an editing buffer of sorts. This is a neat idea, using familiar key bindigs C-c C-c to save and C-c C-k to bail.

edit.gif

Beware, I haven't tested the code with a diverse set of list items, so there's a chance of corrupting the variable content. Improvements to the code are totally welcome.

;;; -*- lexical-binding: t; -*-

(defun ar/edit-list-variable ()
  (interactive)
  (let* ((var (intern
               (completing-read "From variable: "
                                (let (symbols)
                                  (mapatoms
                                   (lambda (sym)
                                     (when (and (boundp sym)
                                                (seqp (symbol-value sym)))
                                       (push sym symbols))))
                                  symbols) nil t)))
         (values (string-join
                  (mapcar #'prin1-to-string (symbol-value var))
                  "\n")))
    (with-current-buffer (get-buffer-create "*eval elisp*")
      (emacs-lisp-mode)
      (local-set-key (kbd "C-c C-c")
                     (lambda ()
                       (interactive)
                       (eval-buffer)
                       (kill-this-buffer)
                       (message "Saved: %s" var)))
      (local-set-key (kbd "C-c C-k") 'kill-this-buffer)
      (erase-buffer)
      (insert (format "(setq %s\n `(%s))" var values))
      (mark-whole-buffer)
      (indent-region (point-min) (point-max))
      (deactivate-mark)
      (switch-to-buffer (current-buffer)))))

Update 2

So hunch was right…

"While this has been a fun exercise, I can't help but think that I'm likely re-inventing the wheel here. Is there something already built-in that I'm missing?"

juicecelery's Reddit commit confirmed it. Thank you! remove-hook is already interactive 🤦‍♂️. TIL 😁

juicecelery was kind enough to point out an improvement in the custom function:

"but I see your improvements, for instance that non list items are removed from the selection."