unofficial mirror of emacs-devel@gnu.org 
 help / color / mirror / code / Atom feed
* Re: [RFC] Editing Lisp through changing indentation
@ 2013-08-28 21:19 Barry OReilly
  2013-08-29  0:14 ` Stefan Monnier
  0 siblings, 1 reply; 15+ messages in thread
From: Barry OReilly @ 2013-08-28 21:19 UTC (permalink / raw)
  To: emacs-devel

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

From http://lists.gnu.org/archive/html/emacs-devel/2013-07/msg00612.html

> 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.

I've been using these commands and find they are useful in practice.
Is the feature suitable for core lisp-mode, or should I create a new
ELPA package to provide the commands?

I've made some updates to the code I posted before:
  • lisp-indent-adjust-parens potentially calls
    indent-for-tab-command, so as it can be suitable for binding to
    TAB
  • Implemented a prefix arg to specify levels of indentation to
    increase or decrease
  • Fixed a couple of edge case bugs

Enable in a lisp-mode-hook like:

  (local-set-key (kbd "TAB") 'lisp-indent-adjust-parens)
  (local-set-key (kbd "<backtab>") 'lisp-dedent-adjust-parens)

Code:

(require 'cl)

(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 ()
  "Adjust a close parentheses of a sexp so as
lisp-indent-adjust-parens can indent that many levels.

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.

If there's no close parens to move, either return nil or allow
scan-error to propogate up."
  (save-excursion
    (let ((deleted-paren-pos
           (save-excursion
             (beginning-of-line)
             (backward-sexp)
             ;; Account for edge case when point has no sexp before it
             (if (bobp)
                 nil
               ;; 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)))))))

(defun adjust-close-paren-for-dedent ()
  "Adjust a close parentheses of a sexp so as
lisp-dedent-adjust-parens can dedent that many levels.

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.

If there's no close parens to move, either return nil or allow
scan-error to propogate up."
  (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
               ;; Needs to work when dedenting in an empty list, in
               ;; which case backward-sexp will signal scan-error and
               ;; sexp-to-close will be nil.
               (condition-case nil
                   (progn (backward-sexp)
                          (point))
                 (scan-error nil))))
          ;; Move point to where to insert close paren
          (if sexp-to-close
              (forward-sexp)
            (backward-up-list)
            (forward-char 1))
          (insert ")")
          ;; The insertion makes deleted-paren-pos off by 1
          (list (1+ deleted-paren-pos)
                (point)))))))

(defun adjust-parens-p ()
  "Whether to adjust parens."
  (save-excursion
    (let ((orig-pos (point)))
      (back-to-indentation)
      (and (not (use-region-p))
           (<= orig-pos (point))))))

(defun adjust-parens-and-indent (adjust-function prefix-arg)
  "Adjust close parens and indent the region over which the parens
moved."
  (let ((region-of-change (list (point) (point))))
    (cl-loop for i from 1 to (or prefix-arg 1)
             with finished = nil
             while (not finished)
             do
             (condition-case err
                 (let ((close-paren-movement
                        (funcall adjust-function)))
                   (if close-paren-movement
                       (setq region-of-change
                             (list (min (car region-of-change)
                                        (car close-paren-movement)
                                        (cadr close-paren-movement))
                                   (max (cadr region-of-change)
                                        (car close-paren-movement)
                                        (cadr close-paren-movement))))
                     (setq finished t)))
               (scan-error (setq finished err))))
    (apply 'indent-region region-of-change))
  (back-to-indentation))

(defun lisp-indent-adjust-parens (&optional prefix-arg)
  "Indent Lisp code to the next level while adjusting sexp balanced
expressions to be consistent.

This command can be bound to TAB instead of indent-for-tab-command. It
potentially calls the latter."
  (interactive "P")
  (if (adjust-parens-p)
      (adjust-parens-and-indent 'adjust-close-paren-for-indent
                                prefix-arg)
    (indent-for-tab-command prefix-arg)))

(defun lisp-dedent-adjust-parens (&optional prefix-arg)
  "Dedent Lisp code to the previous level while adjusting sexp
balanced expressions to be consistent.

Binding to <backtab> (ie Shift-Tab) is a sensible choice."
  (interactive "P")
  (when (adjust-parens-p)
    (adjust-parens-and-indent 'adjust-close-paren-for-dedent
                              prefix-arg)))

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

^ permalink raw reply	[flat|nested] 15+ messages in thread
* [RFC] Editing Lisp through changing indentation
@ 2013-07-19  3:23 Barry OReilly
  2013-07-19  9:23 ` Thien-Thi Nguyen
  0 siblings, 1 reply; 15+ messages in thread
From: Barry OReilly @ 2013-07-19  3:23 UTC (permalink / raw)
  To: emacs-devel

[-- 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 --]

^ permalink raw reply	[flat|nested] 15+ messages in thread

end of thread, other threads:[~2013-08-30  3:24 UTC | newest]

Thread overview: 15+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2013-08-28 21:19 [RFC] Editing Lisp through changing indentation 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
  -- strict thread matches above, loose matches on Subject: below --
2013-07-19  3:23 Barry OReilly
2013-07-19  9:23 ` Thien-Thi Nguyen
2013-07-19 15:58   ` Drew Adams

Code repositories for project(s) associated with this public inbox

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

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).