Sunday, September 19, 2021

Generalize Snarf Tool: How The General Can Be Simpler Than The Specific

Generalize Snarf Tool: How The General Can Be Simpler Than The Specific

1 Executive Summary

The previous article detailed the implementation of a simple function that lets you snarf the contents within a pair of delimiters. That version handled a set of generic delimiters, and errored out if point was not on one of the pre-defined delimiters.

This article shows how that solution can be generalized to cases where point is not on a pre-defined delimiter; in the process, it weighs the pros and cons of usability vs over-generality and shows an implementation that attempts to strike a good balance.

2 The Updated Implementation

(defun snarf-sexp (&optional delete)
  "Snarf the contents between delimiters at point.
Optional interactive prefix arg deletes it."
  (interactive "P")
  (let ((orig (point))
        (pair nil)
        (pairs ;;; We keep predefined pairs for usability:
         '((?< ?>)
           (?\[ ?\])
           (?\( ?\))
           (?{ ?})
           (?\" ?\")
           (?' ?')
           (?` ?')
           (?| ?|)
           (?* ?*)
           (?/ ?/)
           (?- ?-)
           (?_ ?_)
           (?~ ?~)))
        (char (char-after))
        (stab nil))
    (setq pair ;;; But we read a close delimiter  for the general case
          (or (assq char pairs) ;;; Predefined delimiter
              (list char (read-char "Close Delimiter: ")))) ;;; Generality!
    (setq stab (copy-syntax-table))
    (with-syntax-table stab
      (cond
       ((= (cl-first pair) (cl-second pair))
        (modify-syntax-entry (cl-first pair) "\"" ) 
        (modify-syntax-entry (cl-second pair) "\"" ))
       (t
        (modify-syntax-entry (cl-first pair) "(")
        (modify-syntax-entry (cl-second pair) ")")))
      (save-excursion
        (forward-sexp)
        (cond
         (delete
          (kill-region (1+ orig) (1- (point))))
         (t (kill-ring-save (1+ orig) (1- (point)))))))))

3 Key Takeaways

  1. The generalized implementation no longer throws an error if point is not on a pre-defined delimiter.
  2. Instead, it generalizes the implementation to read the close delimiter from the keyboard if char at point is not in the pre-defined list.
  3. We could generalize further by entirely dropping the pre-defined delimiters, but that would hurt usability in the common case where the user would always have to specify the close delimiter.
  4. Note that usability here is not merely to reduce a keystroke; it's more to reduce the cognitive load on the user with respect to having to think about the close delimiter in the common case.

Saturday, September 18, 2021

Snarfing String Within Delimiters With One Defun

Snarfing String Within Delimiters With One Defun

1 Executive Summary

I found that I frequently needed to snarf a string enclosed within delimiters, e.g., URLs in email messages <url>, bolded, italics and other styled text in org-mode etc. I first tried package ciel but found that it did not handle all the delimiters I wanted. However looking into it further revealed that emacs had all the tools needed to reduce the task to a single defun!

2 The Solution

Here is the solution I implemented at emacspeak-wizards-snarf-sexp. invoking this command with point on the opening delimiter snarfs the enclosed string into the kill-ring; an optional prefix arg clears it as well. The code below is the same as in the Emacspeak project, but with emacspeak-specific calls removed:

(defun snarf-sexp-contents (&optional delete)
  "Snarf the contents between delimiters at point.
Optional interactive prefix arg deletes it."
  (interactive "P")
  (let ((orig (point))
        (pair nil)
        (pairs ;;; The delimiter pairs:
         '((?< ?>)
           (?\[ ?\])
           (?\( ?\))
           (?{ ?})
           (?\" ?\")
           (?' ?')
           (?` ?')
           (?| ?|)
           (?* ?*)
           (?/ ?/)
           (?- ?-)
           (?_ ?_)
           (?~ ?~)))
        (char (char-after))
        (stab nil)) ;;; Syntax table we  use
    (unless (setq pair (assoc char pairs)) ;;; Not on a delimiter 
      (error "Point is not on a supported delimiter"))
    (setq stab (copy-syntax-table))
    (with-syntax-table stab
      (cond
       ((= (cl-first pair) (cl-second pair)) ;;;Like quotes
        (modify-syntax-entry (cl-first pair) "\"" ) 
        (modify-syntax-entry (cl-second pair) "\"" ))
       (t;;; Like parens 
        (modify-syntax-entry (cl-first pair) "(")
        (modify-syntax-entry (cl-second pair) ")")))
      (save-excursion;;; We have our sexp 
        (forward-sexp) ;;; Will error out if delims dont match
        (cond
         (delete ;;; Clear sexp contents 
          (kill-region (1+ orig) (1- (point))))
         (t ;;; Copy sexp contents
             (kill-ring-save (1+ orig) (1- (point)))))))))

2.1 Key Take-Aways

  • S-expressions are a key Emacs concept with extensive built-in support.
  • S-expressions are determined by matching delimiters.
  • Delimiters are defined by the syntax-table in effect.
  • Emacs-lisp primitives let us define and manipulate temporary syntax-tables.
  • Putting it all together, the underlying task of snarfing the contents within a pair of delimiters reduces to a few calls to the underlying primitives.