all messages for Emacs-related lists mirrored at yhetil.org
 help / color / mirror / code / Atom feed
From: Barry OReilly <gundaetiapo@gmail.com>
To: emacs-devel@gnu.org
Subject: [RFC] Editing Lisp through changing indentation
Date: Thu, 18 Jul 2013 23:23:47 -0400	[thread overview]
Message-ID: <CAFM41H3fxqb0oBx4gbZr8PVXo72q1rbaN=VW-vBC8tr1AT1ciA@mail.gmail.com> (raw)

[-- Attachment #1: Type: text/plain, Size: 7079 bytes --]

To follow up on [1], I implemented some commands to demonstrate my idea for
editing Lisp.

The motivating idea is this: When reading Lisp, I find I pay attention to
open parens (because foo is not (foo) is not ((foo))) and just the close
parens whose opener is on the same line. When a sexp spans more than one
line, I deduce the close paren from indentation. If that's how we read
Lisp, then why not edit Lisp that way: change the indentation and let the
close parens adjust themselves to be consistent.

This would create an editing experience somewhat like python-mode. There
are differences because lisp-mode knows a bit more due to existing parens.

In the code to follow, I need to address the TODO items as well as your
welcome feedback at this stage.

To try it out on some Lisp code:
  M-x lisp-indent-adjust-sexps
  M-x lisp-dedent-adjust-sexps

[1] http://lists.gnu.org/archive/html/help-gnu-emacs/2013-07/msg00177.html

(defun last-sexp-with-relative-depth (from-pos to-pos rel-depth)
  "Parsing sexps from FROM-POS (inclusive) to TO-POS (exclusive),
return the position of the last sexp that had depth REL-DEPTH relative
to FROM-POS. Returns nil if REL-DEPTH is not reached.

Examples:
  Region:   a (b c (d)) e (f g (h i)) j

  Evaluate: (last-sexp-with-relative-depth pos-a (1+ pos-j) 0)
  Returns:  position of j

  Evaluate: (last-sexp-with-relative-depth pos-a (1+ pos-j) -1)
  Returns:  position of (h i)

This function assumes FROM-POS is not in a string or comment."
  (save-excursion
    (goto-char from-pos)
    (let (the-last-pos
          (parse-state '(0 nil nil nil nil nil nil nil nil)))
      (while (< (point) to-pos)
        (setq parse-state
              (parse-partial-sexp (point)
                                  to-pos
                                  nil
                                  t ; Stop before sexp
                                  parse-state))
        (and (not (eq (point) to-pos))
             (eq (car parse-state) rel-depth)
             (setq the-last-pos (point)))
        ;; The previous parse may not advance. To advance and maintain
        ;; correctness of depth, we parse over the next char.
        (setq parse-state
              (parse-partial-sexp (point)
                                  (1+ (point))
                                  nil
                                  nil
                                  parse-state)))
      the-last-pos)))

(defun adjust-close-paren-for-indent (num-close-parens)
  "Adjust NUM-CLOSE-PARENS number of close parentheses of a sexp so as
lisp-indent-adjust-sexps can indent that many levels.

 [TODO: Reword paragraph when num-close-parens implemented.]
If a close paren was moved, returns a two element list of positions:
where the close paren was moved from and the position following where
it moved to. This allows the caller to know what region potentially
needs reindentation.

If no close parens were moved, returns nil."
  (save-excursion
    (let ((deleted-paren-pos
           (save-excursion
             (beginning-of-line)
             (backward-sexp)
             ;; If the sexp at point is a list,
             ;; delete its closing paren
             (when (eq (scan-lists (point) 1 0)
                       (scan-sexps (point) 1))
               (forward-sexp)
               (delete-char -1)
               (point)))))
      (when deleted-paren-pos
        (let ((sexp-to-close
               (last-sexp-with-relative-depth (point)
                                              (progn (end-of-line)
                                                     (point))
                                              0)))
          (when sexp-to-close
            (goto-char sexp-to-close)
            (forward-sexp))
          ;; Note: when no sexp-to-close found, line is empty. So put
          ;; close paren after point.
          (insert ")")
          (list deleted-paren-pos (point)))))))

;; TODO: When the code settles, consider consolidating with
;; adjust-close-paren-for-indent
(defun adjust-close-paren-for-dedent (num-close-parens)
  (save-excursion
    (let ((deleted-paren-pos
           (save-excursion
             (when (< (point)
                      (progn (up-list)
                             (point)))
               (delete-char -1)
               (point)))))
      (when deleted-paren-pos
        (let ((sexp-to-close
               (progn
                 (backward-sexp)
                 (point))))
          (when sexp-to-close
            (goto-char sexp-to-close)
            (forward-sexp))
          ;; Note: when no sexp-to-close found, line is empty. So put
          ;; close paren after point.
          (insert ")")
          ;; The insertion makes deleted-paren-pos off by 1
          (list (1+ deleted-paren-pos)
                (point)))))))

;; TODO: Look into how to hook into indent-for-tab-command
;; TODO: Take a region interactively: Example of expected region
;; behavior ({} indicates region boundaries)
;;     (let ((x 10) (y (some-func 20)))
;; {     (a 1)
;;       (b 2))}
;; becomes:
;;     (let ((x 10) (y (some-func 20))
;;           (a 1)
;;           (b 2)))
;; TODO: Process the prefix arg: indent that many levels, negative to
;; mean dedent
;; TODO: Write tests
(defun lisp-indent-adjust-sexps (&optional prefix-arg)
  "Indent Lisp code to the next level while adjusting sexp balanced
expressions to be consistent.

Not intended for assignment to the indent-line-function variable. "
  (interactive "P")
  (let ((orig-pos (point)))
    (back-to-indentation)
    (if (> orig-pos (point))
        ;; Effectively don't do anything so as to not obstruct TAB
        ;; completion
        (goto-char orig-pos)
      (let ((close-paren-movement
             (adjust-close-paren-for-indent prefix-arg)))
        (when close-paren-movement
          (apply 'indent-region close-paren-movement)
          ;; Like indent-for-tab-command, this command will leave
          ;; point at "back to indentation". This call is necessary
          ;; because indent-region's save-excursion marker can get
          ;; moved to the beginning of line due to how the indentation
          ;; whitespace is inserted.
          (back-to-indentation))))))

;; TODO: Investigate how to invoke this with DEL key and get old DEL
;; behavior when not dedenting (ie not in the indentation). Introduce
;; a dedent-for-del-command?
;; Note: Dedent will not take a region because:
;;   - Don't want to conflict with delete-selection-mode
;;   - Doesn't need it as much as indent with TAB does
;; TODO: Fix duplication when the code settles more
(defun lisp-dedent-adjust-sexps (&optional prefix-arg)
  (interactive "P")
  (let ((orig-pos (point)))
    (back-to-indentation)
    (if (> orig-pos (point))
        ;; Effectively don't do anything, hope to allow ordinary DEL
        (goto-char orig-pos)
      (let ((close-paren-movement
             (adjust-close-paren-for-dedent prefix-arg)))
        (when close-paren-movement
          (apply 'indent-region (nreverse close-paren-movement))
          (back-to-indentation))))))

[-- Attachment #2: Type: text/html, Size: 7934 bytes --]

             reply	other threads:[~2013-07-19  3:23 UTC|newest]

Thread overview: 15+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2013-07-19  3:23 Barry OReilly [this message]
2013-07-19  9:23 ` [RFC] Editing Lisp through changing indentation Thien-Thi Nguyen
2013-07-19 15:58   ` Drew Adams
  -- strict thread matches above, loose matches on Subject: below --
2013-08-28 21:19 Barry OReilly
2013-08-29  0:14 ` Stefan Monnier
2013-08-29 19:50   ` Barry OReilly
2013-08-29 20:04     ` Stefan Monnier
2013-08-29 20:40       ` Barry OReilly
2013-08-29 22:14         ` Stefan Monnier
2013-08-29 22:30         ` Andreas Schwab
2013-08-29 22:39           ` Stefan Monnier
2013-08-29 22:49             ` Barry OReilly
2013-08-30  2:23               ` Stefan Monnier
2013-08-30  2:48                 ` Barry OReilly
2013-08-30  3:24                   ` Stefan Monnier

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to='CAFM41H3fxqb0oBx4gbZr8PVXo72q1rbaN=VW-vBC8tr1AT1ciA@mail.gmail.com' \
    --to=gundaetiapo@gmail.com \
    --cc=emacs-devel@gnu.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this external index

	https://git.savannah.gnu.org/cgit/emacs.git
	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.