unofficial mirror of emacs-devel@gnu.org 
 help / color / mirror / code / Atom feed
* [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

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

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

() Barry OReilly <gundaetiapo@gmail.com>
() Thu, 18 Jul 2013 23:23:47 -0400

   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.

I think this explanation might be clearer if you say "close paren from
the preceding sexp".  I only understood that nuance after playing with
the commands (which work fine, btw).

   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.

Well, here i have a different pov.  When i edit Lisp, indentation is the
last thing to fall into place (i.e., a "fixup" operation, via ‘C-M-q’);
i prefer to grok/munge whitespace-agnostic sexps first and foremost, via
the usual C-M-{SPC,k,f,b,n,p,u,d}, etc.  I include "grok" because i read
code actively, moving in and around it, watching what mic-paren bolds.

The commands you provide can be described as "(un)tucking the sexp at
point into the preceding sexp at the tail position", which translates to
adding-to /removing-from a function call an argument that happens to be
situated following that function call.  That's not something i do often.

But anyway, it is good to see hacking of this kind.  Don't stop!

-- 
Thien-Thi Nguyen
   GPG key: 4C807502
   (if you're human and you know it)
      read my lisp: (responsep (questions 'technical)
                               (not (via 'mailing-list)))
                     => nil

[-- Attachment #2: Type: application/pgp-signature, Size: 197 bytes --]

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

* RE: [RFC] Editing Lisp through changing indentation
  2013-07-19  9:23 ` Thien-Thi Nguyen
@ 2013-07-19 15:58   ` Drew Adams
  0 siblings, 0 replies; 15+ messages in thread
From: Drew Adams @ 2013-07-19 15:58 UTC (permalink / raw)
  To: Thien-Thi Nguyen, Barry OReilly; +Cc: emacs-devel

FWIW, I agree with what TTN said, including the encouragement
to continue.

For me, it's the paren/structure that takes precedence, and
the indentation that follows.  I use TAB and C-M-q, among
other things, to see whether I messed up parentheses.


FWIW2, you might also be interested in a gimmick that Franz
Lisp used to have (before it moved to Common), and perhaps
some other Lisps:

A right bracket, `]', acted as a super right paren, being
equivalent to sufficient right parens to close up the
outermost open list.  E.g.,

 (foo (bar 1 (2 3) ((a . b) (c (d (e . f]

was equivalent to

 (foo (bar 1 (2 3) ((a . b) (c (d (e . f))))))

It was mainly a typing convenience, IIRC, i.e., for
interactive use, but people did just leave the brackets in
files also (IIRC).

Another possibility (and this might have been available too;
I don't recall) would be for typing `]' to insert the right
number of right parens, instead of just inserting a `]' and
having that be interpreted as the right number of right
parens.

(I don't recall that being available, but even if it were I
suspect that most fans of `]' would have preferred to see
the `]' and be able to delete it as a single char etc.)

Such a super-paren might be considered a convenience by some,
but I never made much use of it, and I certainly don't miss it.
It is most useful, IIRC, in a REPL, i.e., interactively, where
you might not have the ability to TAB or C-M-q to fix up
indentation and see what's what wrt parens.

(Common Lisp takes a smarter view of naturally paired
delimiter chars such as `[', `]', realizing that they are
rare and that Lisp users often make use of them when defining
new languages or language constructs.  But then, Common Lisp
has reader macros...  It's poor cousin Emacs Lisp does not.)



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

* 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

* Re: [RFC] Editing Lisp through changing indentation
  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
  0 siblings, 1 reply; 15+ messages in thread
From: Stefan Monnier @ 2013-08-29  0:14 UTC (permalink / raw)
  To: Barry OReilly; +Cc: emacs-devel

> Is the feature suitable for core lisp-mode, or should I create a new
> ELPA package to provide the commands?

I suggest you start with an ELPA package.  If it turns out to be
popular, we can later include it in Emacs.

But if you need changes in lisp-mode.el (for example), do not hesitate
to request them,


        Stefan



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

* Re: [RFC] Editing Lisp through changing indentation
  2013-08-29  0:14 ` Stefan Monnier
@ 2013-08-29 19:50   ` Barry OReilly
  2013-08-29 20:04     ` Stefan Monnier
  0 siblings, 1 reply; 15+ messages in thread
From: Barry OReilly @ 2013-08-29 19:50 UTC (permalink / raw)
  To: Stefan Monnier; +Cc: emacs-devel


[-- Attachment #1.1: Type: text/plain, Size: 3650 bytes --]

> I suggest you start with an ELPA package. If it turns out to be
> popular, we can later include it in Emacs.

Ok, I'll call it adjust-parens.

In ELPA, 'make archive' expects a package ChangeLog, even while
existing packages don't have one.

  $ make archive
  rm -r archive-tmp
  rm: cannot remove `archive-tmp': No such file or directory
  make: [archive-tmp] Error 1 (ignored)
  mkdir -p archive-tmp
  cp -a packages/. archive-tmp/packages
  make  process-archive
  make[1]: Entering directory `/redacted/linux/boreilly/sw/elpa'
  # FIXME, we could probably speed this up significantly with
  # rules like "%.tar: ../%/ChangeLog" so we only rebuild the packages
  # that have indeed changed.
  cd archive-tmp/packages; \
            emacs --batch -l
/redacted/linux/boreilly/sw/elpa/admin/archive-contents.el \
              -f batch-make-archive
  Skipping non-package file README
  Wrote /home/boreilly/l/sw/elpa/archive-tmp/packages/ack/ack-pkg.el
  Error in adaptive-wrap: (file-error "Opening input file" "no such file or
directory"
"/home/boreilly/l/sw/elpa/archive-tmp/packages/adaptive-wrap/ChangeLog")
  make[1]: *** [process-archive] Error 255
  make[1]: Leaving directory `/redacted/linux/boreilly/sw/elpa'
  make: *** [archive] Error 2

I fixed it with this patch (git diff -w):

diff --git a/admin/archive-contents.el b/admin/archive-contents.el
index 2d588e9..640c285 100644
--- a/admin/archive-contents.el
+++ b/admin/archive-contents.el
@@ -206,6 +206,7 @@ Rename DIR/PKG.el to PKG-VERS.el, delete DIR, and
return the descriptor."
       (re-search-backward "^;;;.*ends here")
       (re-search-backward "^(provide")
       (skip-chars-backward " \t\n")
+      (when (file-readable-p cl)
         (insert "\n\n;;;; ChangeLog:\n\n")
         (let* ((start (point))
                (end (copy-marker start t)))
@@ -213,7 +214,7 @@ Rename DIR/PKG.el to PKG-VERS.el, delete DIR, and
return the descriptor."
           (goto-char end)
           (unless (bolp) (insert "\n"))
           (while (progn (forward-line -1) (>= (point) start))
-          (insert ";; ")))
+            (insert ";; "))))
       (set (make-local-variable 'backup-inhibited) t)
       (basic-save-buffer)               ;Less chatty than save-buffer.
       (kill-buffer)))

Shall I commit this?

After that, list-packages shows the new adjust-parens package in the
local ELPA archive.

Here are some ELPA README redlines:

diff --git a/README b/README
index 097e430..3d01912 100644
--- a/README
+++ b/README
@@ -29,9 +29,9 @@ each package.

 *** Add a multi-file package as a directory, packages/NAME.

-*** Commit your changes the usual way ("bzr add", "bzr commit", etc).
+*** Commit your changes the usual way ("git add", "git commit", etc).

-Changes in the Bzr repository do not immediately propagate to the
+Changes in the Git repository do not immediately propagate to the
 user-facing archive (what users see when they do `M-x list-packages').
 That is done by deploying the archive.

@@ -67,7 +67,7 @@ and adds them to the archive.

 ** To access a deployed archive

-To access the archive via HTPP, have a symlink (say) /var/www/packages
+To access the archive via HTTP, have a symlink (say) /var/www/packages
 pointing to DEST/packages, and set up Emacs with

   (setq package-archives '(("new-elpa" . "http://foo.com/packages")))

The ELPA README mentions having a "site" build target, but:

  $ make site
  make: *** No rule to make target `site'.  Stop.

If the site target to Make is indeed gone, I'll remove wording from
the README about it too.

With no objections, I'll commit the above diffs and the attached
adjust-parens.el file to ELPA.

[-- Attachment #1.2: Type: text/html, Size: 4293 bytes --]

[-- Attachment #2: adjust-parens.el --]
[-- Type: application/octet-stream, Size: 10199 bytes --]

;;; adjust-parens.el --- Indent and dedent Lisp code, automatically adjust close parens -*- lexical-binding: t; -*-

;; Copyright (C) 2013  Free Software Foundation, Inc.

;; Author: Barry O'Reilly <gundaetiapo@gmail.com>
;; Version: 1.0

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:
;;
;; This package provides commands for indenting and dedenting Lisp
;; code such that close parentheses are automatically adjusted to be
;; consistent with the new level of indentation.
;;
;; When reading Lisp, the programmer pays attention to open parens and
;; the close parens on the same line. But when a sexp spans more than
;; one line, she deduces the close paren from indentation alone. Given
;; that's how we read Lisp, this package aims to enable editing Lisp
;; similarly: automatically adjust the close parens programmers ignore
;; when reading. A result of this is an editing experience somewhat
;; like python-mode, which also offers "indent" and "dedent" commands.
;; There are differences because lisp-mode knows more due to existing
;; parens.
;;
;; To use:
;;   (require 'adjust-parens)
;;
;; This binds two keys in Lisp Mode:
;;   (local-set-key (kbd "TAB") 'lisp-indent-adjust-parens)
;;   (local-set-key (kbd "<backtab>") 'lisp-dedent-adjust-parens)
;;
;; lisp-indent-adjust-parens potentially calls indent-for-tab-command
;; (the usual binding for TAB in Lisp Mode). Thus it should not
;; interfere with other TAB features like completion-at-point.
;;
;; Some examples follow. | indicates the position of point.
;;
;;   (let ((x 10) (y (some-func 20))))
;;   |
;;
;; After one TAB:
;;
;;   (let ((x 10) (y (some-func 20)))
;;     |)
;;
;; After three more TAB:
;;
;;   (let ((x 10) (y (some-func 20
;;                              |))))
;;
;; After two Shift-TAB to dedent:
;;
;;   (let ((x 10) (y (some-func 20))
;;         |))
;;
;; When dedenting, the sexp may have sibling sexps on lines below. It
;; makes little sense for those sexps to stay at the same indentation,
;; because they cannot keep the same parent sexp without being moved
;; completely. Thus they are dedented too. An example of this:
;;
;;   (defun func ()
;;     (save-excursion
;;       (other-func-1)
;;       |(other-func-2)
;;       (other-func-3)))
;;
;; After Shift-TAB:
;;
;;   (defun func ()
;;     (save-excursion
;;       (other-func-1))
;;     |(other-func-2)
;;     (other-func-3))
;;
;; If you indent again with TAB, the sexps siblings aren't indented:
;;
;;   (defun func ()
;;     (save-excursion
;;       (other-func-1)
;;       |(other-func-2))
;;     (other-func-3))
;;
;; Thus TAB and Shift-TAB are not exact inverse operations of each
;; other, though they often seem to be.

;;; Code:

;; Future work:
;;   - Consider taking a region as input in order to indent a sexp and
;;     its siblings in the region. Dedenting would not take a region.
;;   - Write tests

(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)))

(add-hook 'emacs-lisp-mode-hook
          (lambda ()
            (local-set-key (kbd "TAB") 'lisp-indent-adjust-parens)
            (local-set-key (kbd "<backtab>") 'lisp-dedent-adjust-parens)))

(provide 'adjust-parens)

;;; adjust-parens.el ends here

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

* Re: [RFC] Editing Lisp through changing indentation
  2013-08-29 19:50   ` Barry OReilly
@ 2013-08-29 20:04     ` Stefan Monnier
  2013-08-29 20:40       ` Barry OReilly
  0 siblings, 1 reply; 15+ messages in thread
From: Stefan Monnier @ 2013-08-29 20:04 UTC (permalink / raw)
  To: Barry OReilly; +Cc: emacs-devel

> In ELPA, 'make archive'

Why do you need to run that?

> expects a package ChangeLog, even while
> existing packages don't have one.

At the time it is run, there should be one.

> Shall I commit this?

No, I'd rather get an error if the ChangeLog is somehow missing.

> After that, list-packages shows the new adjust-parens package in the
> local ELPA archive.

The way you should do it is just "make" and then point your archive to
.../elpa/packages.

> -*** Commit your changes the usual way ("bzr add", "bzr commit", etc).
> +*** Commit your changes the usual way ("git add", "git commit", etc).
> -Changes in the Bzr repository do not immediately propagate to the
> +Changes in the Git repository do not immediately propagate to the
>  user-facing archive (what users see when they do `M-x list-packages').
>  That is done by deploying the archive.
> @@ -67,7 +67,7 @@ and adds them to the archive.
>  ** To access a deployed archive
> -To access the archive via HTPP, have a symlink (say) /var/www/packages
> +To access the archive via HTTP, have a symlink (say) /var/www/packages
>  pointing to DEST/packages, and set up Emacs with

Oops, thanks.  Feel free to install these fixes.

> The ELPA README mentions having a "site" build target, but:

>   $ make site
>   make: *** No rule to make target `site'.  Stop.

> If the site target to Make is indeed gone, I'll remove wording from
> the README about it too.

Right, it should be replaced with the new steps of running just "make"
and pointing to .../elpa/packages rather than .../elpa/site.

> With no objections, I'll commit the above diffs and the attached
> adjust-parens.el file to ELPA.

Sounds good, thank you.


        Stefan



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

* Re: [RFC] Editing Lisp through changing indentation
  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
  0 siblings, 2 replies; 15+ messages in thread
From: Barry OReilly @ 2013-08-29 20:40 UTC (permalink / raw)
  To: Stefan Monnier; +Cc: emacs-devel

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

>> In ELPA, 'make archive'

> Why do you need to run that?

The README led me think that's how to build an archive. The README is
silent about running the default make target, so clearly needs an
update.

When I 'make':

  $ make
  # Do them in a sub-make, so that autoloads are done first.
  make elcs
  make[1]: Entering directory `/psd15/linux/boreilly/sw/elpa'
  EMACS -f batch-byte-compile packages/dict-tree/dict-tree.el

  In toplevel form:
  packages/dict-tree/dict-tree.el:54:1:Error: Cannot open load file: trie
  make[1]: *** [packages/dict-tree/dict-tree.elc] Error 1
  make[1]: Leaving directory `/psd15/linux/boreilly/sw/elpa'
  make: *** [all-in-place] Error 2

For starters, the output is wrong about the command issued.

diff --git a/GNUmakefile b/GNUmakefile
index 0fac72b..561c2f3 100644
--- a/GNUmakefile
+++ b/GNUmakefile
@@ -123,8 +123,7 @@ elcs := $(call SET-diff, $(naive_elcs), $(patsubst
%.el, %.elc, $(nbc_els)))

 # '(dolist (al (quote ($(patsubst %, "%", $(autoloads))))) (load
(expand-file-name al) nil t))'
 %.elc: %.el
-       @echo 'EMACS -f batch-byte-compile $<'
-       @$(EMACS) \
+       $(EMACS) \
            --eval "(setq package-directory-list '(\"$(abspath
packages)\"))" \
            --eval '(package-initialize)' \
            -L $(dir $@) -f batch-byte-compile $<

When I manually add the necessary -L flags (-L packages/trie/ -L
packages/heap/ -L packages/tNFA/ -L packages/queue/) then I could byte
compile with warnings. But how is it supposed to get the load paths?

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

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

* Re: [RFC] Editing Lisp through changing indentation
  2013-08-29 20:40       ` Barry OReilly
@ 2013-08-29 22:14         ` Stefan Monnier
  2013-08-29 22:30         ` Andreas Schwab
  1 sibling, 0 replies; 15+ messages in thread
From: Stefan Monnier @ 2013-08-29 22:14 UTC (permalink / raw)
  To: Barry OReilly; +Cc: emacs-devel

>>> In ELPA, 'make archive'
>> Why do you need to run that?
> The README led me think that's how to build an archive. The README is
> silent about running the default make target, so clearly needs an
> update.

Indeed.

> When I 'make':

>   $ make
>   # Do them in a sub-make, so that autoloads are done first.
>   make elcs
>   make[1]: Entering directory `/psd15/linux/boreilly/sw/elpa'
>   EMACS -f batch-byte-compile packages/dict-tree/dict-tree.el

>   In toplevel form:
>   packages/dict-tree/dict-tree.el:54:1:Error: Cannot open load file: trie
>   make[1]: *** [packages/dict-tree/dict-tree.elc] Error 1
>   make[1]: Leaving directory `/psd15/linux/boreilly/sw/elpa'
>   make: *** [all-in-place] Error 2

Hmm...

> For starters, the output is wrong about the command issued.

This part is on purpose.

> When I manually add the necessary -L flags (-L packages/trie/ -L
> packages/heap/ -L packages/tNFA/ -L packages/queue/) then I could byte
> compile with warnings. But how is it supposed to get the load paths?

The

            --eval "(setq package-directory-list '(\"$(abspath
packages)\"))" \
            --eval '(package-initialize)' \

should add those packages as if they were installed (tho not compiled
yet), so the directories should already be added to the load-path.


        Stefan



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

* Re: [RFC] Editing Lisp through changing indentation
  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
  1 sibling, 1 reply; 15+ messages in thread
From: Andreas Schwab @ 2013-08-29 22:30 UTC (permalink / raw)
  To: Barry OReilly; +Cc: Stefan Monnier, emacs-devel

Barry OReilly <gundaetiapo@gmail.com> writes:

> When I manually add the necessary -L flags (-L packages/trie/ -L
> packages/heap/ -L packages/tNFA/ -L packages/queue/) then I could byte
> compile with warnings. But how is it supposed to get the load paths?

Do you mean this?

diff --git a/GNUmakefile b/GNUmakefile
index 0fac72b..4ff3d31 100644
--- a/GNUmakefile
+++ b/GNUmakefile
@@ -127,7 +127,8 @@ elcs := $(call SET-diff, $(naive_elcs), $(patsubst %.el, %.elc, $(nbc_els)))
 	@$(EMACS) \
 	    --eval "(setq package-directory-list '(\"$(abspath packages)\"))" \
 	    --eval '(package-initialize)' \
-	    -L $(dir $@) -f batch-byte-compile $<
+	    $(patsubst %, -L %, $(filter-out %/cl-lib, $(pkgs))) \
+	    -f batch-byte-compile $<
 
 .PHONY: elcs
 elcs: $(elcs)

Andreas.

-- 
Andreas Schwab, schwab@linux-m68k.org
GPG Key fingerprint = 58CA 54C7 6D53 942B 1756  01D3 44D5 214B 8276 4ED5
"And now for something completely different."



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

* Re: [RFC] Editing Lisp through changing indentation
  2013-08-29 22:30         ` Andreas Schwab
@ 2013-08-29 22:39           ` Stefan Monnier
  2013-08-29 22:49             ` Barry OReilly
  0 siblings, 1 reply; 15+ messages in thread
From: Stefan Monnier @ 2013-08-29 22:39 UTC (permalink / raw)
  To: Andreas Schwab; +Cc: Barry OReilly, emacs-devel

>  	    --eval "(setq package-directory-list '(\"$(abspath packages)\"))" \
>  	    --eval '(package-initialize)' \
> -	    -L $(dir $@) -f batch-byte-compile $<
> +	    $(patsubst %, -L %, $(filter-out %/cl-lib, $(pkgs))) \
> +	    -f batch-byte-compile $<
 
Calling package-initialize with package-directory-list set to
'(\"$(abspath packages)\") should make the rest unnecessary (and should
skip cl-lib as well ;-).


        Stefan



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

* Re: [RFC] Editing Lisp through changing indentation
  2013-08-29 22:39           ` Stefan Monnier
@ 2013-08-29 22:49             ` Barry OReilly
  2013-08-30  2:23               ` Stefan Monnier
  0 siblings, 1 reply; 15+ messages in thread
From: Barry OReilly @ 2013-08-29 22:49 UTC (permalink / raw)
  To: Stefan Monnier; +Cc: Andreas Schwab, emacs-devel

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

>> For starters, the output is wrong about the command issued.

> This part is on purpose.

Why tell the user the wrong thing?

-           -L $(dir $@) -f batch-byte-compile $<
+           $(patsubst %, -L %, $(filter-out %/cl-lib, $(pkgs))) \
+           -f batch-byte-compile $<

This allowed make to run to completion.

Using a sufficiently new Emacs executable helped too.

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

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

* Re: [RFC] Editing Lisp through changing indentation
  2013-08-29 22:49             ` Barry OReilly
@ 2013-08-30  2:23               ` Stefan Monnier
  2013-08-30  2:48                 ` Barry OReilly
  0 siblings, 1 reply; 15+ messages in thread
From: Stefan Monnier @ 2013-08-30  2:23 UTC (permalink / raw)
  To: Barry OReilly; +Cc: Andreas Schwab, emacs-devel

>>> For starters, the output is wrong about the command issued.
>> This part is on purpose.
> Why tell the user the wrong thing?

> -           -L $(dir $@) -f batch-byte-compile $<
> +           $(patsubst %, -L %, $(filter-out %/cl-lib, $(pkgs))) \
> +           -f batch-byte-compile $<

> This allowed make to run to completion.

But as I said, AFAIK it should not be necessary.  So I think what you're
seeing is a symptom of a problem elsewhere and the patch above is just
a workaround.


        Stefan



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

* Re: [RFC] Editing Lisp through changing indentation
  2013-08-30  2:23               ` Stefan Monnier
@ 2013-08-30  2:48                 ` Barry OReilly
  2013-08-30  3:24                   ` Stefan Monnier
  0 siblings, 1 reply; 15+ messages in thread
From: Barry OReilly @ 2013-08-30  2:48 UTC (permalink / raw)
  To: Stefan Monnier; +Cc: Andreas Schwab, emacs-devel

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

> But as I said, AFAIK it should not be necessary. So I think what
> you're seeing is a symptom of a problem elsewhere and the patch
> above is just a workaround.

I had an Emacs executable from trunk 2013-06-05 in my path. Using a
newer one solved the issue.

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

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

* Re: [RFC] Editing Lisp through changing indentation
  2013-08-30  2:48                 ` Barry OReilly
@ 2013-08-30  3:24                   ` Stefan Monnier
  0 siblings, 0 replies; 15+ messages in thread
From: Stefan Monnier @ 2013-08-30  3:24 UTC (permalink / raw)
  To: Barry OReilly; +Cc: Andreas Schwab, emacs-devel

>> But as I said, AFAIK it should not be necessary. So I think what
>> you're seeing is a symptom of a problem elsewhere and the patch
>> above is just a workaround.
> I had an Emacs executable from trunk 2013-06-05 in my path. Using a
> newer one solved the issue.

Ah, that could explain it, indeed.  The "make" code does rely to some
extent on some of the recentish changes I've made to package.el.


        Stefan



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