Skip to content

Examples

okamsn edited this page Aug 21, 2021 · 4 revisions

NOTE: As Loopy develops, these examples and results might become out of date. For the most up-to-date information, refer to this package's documentation, which is installed with the package as the Info file loopy.


Below are some examples on how one could use loopy. Feel free to add you own.


Table of Contents


Working with Selectrum

Selectrum is a completion framework that aims to work better with Emacs's existing completion features. Completion usually involves working with a list of candidates, and Loopy is a good tool for producing candidates.

The below commands need the following to run:

(require 'loopy)
(require 'selectrum)
(defvar selectrum-should-sort)
(defvar selectrum-swiper-history)
(defvar selectrum-outline-history)

(declare-function selectrum-read "ext:selectrum")
(autoload 'selectrum-read "ext:selectrum")

Other versions of these commands can be found on the Selectrum wiki.

A swiper-like command

(defun loopy-examples-selectrum-swiper ()
  "Search for a matching line and jump to the beginning of its text.
The default candidate is the line closest to the current one.
Obeys narrowing."
  (interactive)
  (loopy (with (current-line-number (line-number-at-pos (point) t))
               (buffer-text-lines (split-string (buffer-string) "\n"))
               (number-format
                (format "L%%0%dd: "
                        (length (number-to-string
                                 (line-number-at-pos (point-max) t))))))

         ;; Loop through lines in the buffer.
         (list line-text buffer-text-lines)
         (expr line-num (line-number-at-pos (point-min) t) (1+ line-num))

         (unless (string-empty-p line-text)

           ;; Once the distance between candidates and the current line starts
           ;; to increase, select the previously formatted candidate (i.e., the
           ;; candidate closest to the current line) as the default.
           ;;
           ;; There are a few different ways that you could express this idea.
           (when (null default-candidate)
             (expr prev-dist +1.0e+INF dist-to-default-cand)
             (expr dist-to-default-cand (abs (- current-line-number
                                                line-num)))
             (when (> dist-to-default-cand prev-dist)
               ;; Note that we don't need to declare `default-candidate'.
               (expr default-candidate formatted-candidate)))

           ;; Format the current line.
           (expr formatted-candidate
                 (propertize
                  line-text
                  'selectrum-candidate-display-prefix
                  (propertize (format number-format line-num)
                              'face 'completions-annotations)
                  'line-num line-num))

           ;; Create a list of formatted candidates.
           (collect formatted-candidate))

         (finally-do
          (let* ((selectrum-should-sort nil)
                 (chosen-line-number
                  (get-text-property
                   0 'line-num
                   (selectrum-read "Jump to matching line: "
                                   loopy-result
                                   :default-candidate default-candidate
                                   :history 'selectrum-swiper-history
                                   :require-match t
                                   :no-move-default-candidate t))))
            (push-mark (point) t)
            (forward-line (- chosen-line-number current-line-number))
            (beginning-of-line-text 1)))))

An outline-like command

(defcustom selectrum-outline-formats
  ;; Groups: (1) level determinant, (2) heading text.
  ;; The top level is 0, for a zero-length determinant.
  '((emacs-lisp-mode
     . "^;;;\\(?1:;*\\)[[:blank:]]*\\(?2:[[:alnum:]][^z-a]*\\)\\'")
    (lisp-mode
     . "^;;;\\(?1:;*\\)[[:blank:]]*\\(?2:[[:alnum:]][^z-a]*\\)\\'")
    (gfm-mode ; Github Flavored Markdown
     . "^#\\(?1:#*\\)[[:blank:]]*\\(?2:[[:alnum:]][^z-a]*\\)\\'")
    (markdown-mode
     . "^#\\(?1:#*\\)[[:blank:]]*\\(?2:[[:alnum:]][^z-a]*\\)\\'")
    (outline-mode
     . "^\\*\\(?1:\\**\\)[[:blank:]]*\\(?2:[[:alnum:]][^z-a]*\\)\\'")
    ;; For Org, see also ‘org-goto’.
    (org-mode
     . "^\\*\\(?1:\\**\\)[[:blank:]]*\\(?2:[[:alnum:]][^z-a]*\\)\\'")
    (python-mode
     . "^##\\(?1:\\**\\|#*\\)[[:blank:]]*\\(?2:[[:alnum:]][^z-a]*\\)\\'"))
  "Alist of regexps used for identifying outline headings in each major mode.

The ‘car’ of an item in the list should be a symbol of the major mode.
The ‘cdr’ should be a regular expression with two required match groups:
1. Match group 1, whose length determines the outline level of that heading.
   For best formatting, the top level should be level 0 for zero length.
2. Match group 2, which is the actual heading text.

A heading is assumed to be on only one line."
  :group 'selectrum
  :type '(alist
          :key-type (symbol :tag "Major mode symbol")
          :value-type (string :tag "Regexp")))

(defun loopy-examples-selectrum-outline ()
  "Jump to a heading.  Regexps are pre-defined.  Obeys narrowing."
  (interactive)
  (let ((heading-regexp (alist-get major-mode selectrum-outline-formats)))
    ;; Signal a `user-error' if we don't have a regexp for this major mode.  We
    ;; could also check this in a `before-do' macro argument to `loopy', but
    ;; that would happen /after/ the `with' variables are defined, which is
    ;; inefficient.
    (unless heading-regexp
      (user-error "Selectrum-outline: No heading regexp for mode %s" major-mode))
    (save-match-data
      (loopy
       (with (default-heading)
             (buffer-lines (split-string (buffer-string) "\n"))
             (line-number-format
              (concat "L%0"
                      (number-to-string
                       (length (number-to-string (length buffer-lines))))
                      "d: "))
             (current-line-number (line-number-at-pos (point))))

       ;; Iterate through a list of lines.
       (list text-line buffer-lines)
       ;; Keep track of the line number of the candidate.
       (expr line-number 1 (1+ line-number))

       (when (string-match heading-regexp text-line)
         (expr prev-heading-text nil heading-text)
         (expr prev-heading-level nil heading-level)

         (expr heading-text (match-string-no-properties 2 text-line))
         (expr heading-level (length (match-string 1 text-line)))

         ;; Decide whether to update the prefix list and the previous
         ;; heading level.
         (cond
          ;; If we've moved to a greater level (further down the tree),
          ;; add the previous heading to the heading prefix list so
          ;; that we can prepend it to the current heading when
          ;; formatting.
          ((> heading-level (or prev-heading-level
                                heading-level))
           (push-into backwards-prefix-list
                      prev-heading-text))
          ((< heading-level (or prev-heading-level
                                heading-level))
           ;; Otherwise, if we've moved to a lower level (higher up the
           ;; tree), and need to remove the most recently added prefix
           ;; from the list (i.e., go from '(c b a) back to '(b a)).
           (loop (repeat (- prev-heading-level heading-level))
                 (do (pop backwards-prefix-list)))))

         ;; Finally, add to list of formatted headings.
         ;; Create heading of form "L#: a/b/c" as:
         ;; - having a text property holding the line number
         ;; - prepended with a formatted line number,
         ;;   with the face ‘completions-annotations’.
         (expr formatted-heading
               (propertize
                (concat (string-join (reverse backwards-prefix-list) "/")
                        (and backwards-prefix-list "/")
                        heading-text)
                'line-number line-number
                'selectrum-candidate-display-prefix
                (propertize
                 (format line-number-format line-number)
                 'face 'completions-annotations)))

         ;; If needed, set default candidate.
         (when (and (null default-heading)
                    (> line-number current-line-number))
           (expr default-heading formatted-heading))

         ;; Collect formatted heading into `loopy-result'.
         (collect formatted-heading))

       (finally-do
        (let* ((selectrum-should-sort nil)
               (chosen-heading
                (selectrum-read "Jump to heading: "
                                loopy-result
                                :default-candidate default-heading
                                :history 'selectrum-outline-history
                                :require-match t
                                :no-move-default-candidate t)))
          ;; Push mark, in case we want to return to current location.  This
          ;; needs to happen /after/ the user has made it clear that they
          ;; want to go somewhere.
          (push-mark (point) t)
          ;; Move to beginning of chosen line.
          (forward-line (- (get-text-property 0 'line-number chosen-heading)
                           current-line-number))
          (beginning-of-line-text 1)))))))