unofficial mirror of emacs-devel@gnu.org 
 help / color / mirror / code / Atom feed
* [Proposed Minor-mode] Speed of thought Lisp (was: Abbrevs for the most frequent elisp symbols)
@ 2015-01-24 16:38 Artur Malabarba
  2015-01-25 14:49 ` [Proposed Minor-mode] Speed of thought Lisp Stefan Monnier
  2015-01-27 12:28 ` Oleh Krehel
  0 siblings, 2 replies; 16+ messages in thread
From: Artur Malabarba @ 2015-01-24 16:38 UTC (permalink / raw)
  To: emacs-devel


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

For a while now, I've been using a system that leads to very fluid elisp
coding. The focus is on writing lisp as fast as you can think of it,
without being slowed down by your typing speed.
Thus the name: Speed-of-thought Lisp.

I've packaged it into a minor mode, which I'm proposing for inclusion into
Emacs. I really can't overstate how much I've come to like this little
mode, so hopefully some here will find it useful too.

The mode is quite simple, and is composed of two parts:
Abbrevs

A large number of abbrevs, almost identical to what was discussed back in
December on a thread in help-emacs. These abbrevs expand function initials
to their name. The actual abbrevs are completely arbitrary (I added them as
I ran into them), so I'm perfectly open to changing them based on general
usage frequency.

A few examples (currently they are 96 in total):

   - “wcb” -> “with-current-buffer”
   - “i” -> “insert”
   - “r” -> “require '”
   - “a” -> “and”

However, these are defined in a way such that they ONLY expand in a place
where you would use a function, so hitting SPC after “(r” expands to
“(require '”, but hitting SPC after “(delete-region r” will NOT expand the
`r', because that's obviously not a function.

Furtheromre, “#'r” will expand to “#'require” (note how it ommits that
extra quote, since it would be useless here).
Commands

It also defines 4 commands, which really fit into this “follow the
thought-flow” way of writing. The bindings are as follows, I understand
these don't fully adhere to conventions, and I'd appreaciate suggestions on
better bindings.

   - M-RET :: Break line, and insert “()” with point in the middle.
   - C-RET :: Do `forward-up-list', then do M-RET.

Hitting RET followed by a `(' was one of the most common key sequences for
me while writing elisp, so giving it a quick-to-hit key was a significant
improvement.

   - C-c f :: Find function under point. If it is not defined, create a
   definition for it below the current function and leave point inside.
   - C-c v :: Same, but for variable.

With these commands, you just write your code as you think of it. Once you
hit a “stop-point” of sorts in your tought flow, you hit C-c f/v on any
undefined functions/variables, write their definitions, and hit C-u C-SPC
to go back to the main function.
Small Example

With the above (assuming you use something like paredit or
electric-pair-mode), if you write:

    ( w t b M-RET i SPC text

You get

    (with-temp-buffer
      (insert text))


Cheers to all, and please let me hear your thoughts!
Artur

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

[-- Attachment #2: sotlisp.el --]
[-- Type: text/x-emacs-lisp, Size: 16862 bytes --]

;;; sotlisp.el --- Write lisp at the speed of thought.  -*- lexical-binding: t; -*-

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

;; Author: Artur Malabarba  <bruce.connor.am@gmail.com>
;; Keywords: convenience, lisp
;; Package-Requires: ((emacs "24.1"))

;; 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 defines a new global minor-mode `speed-of-thought-mode', which
;; activates locally on any supported buffer.  Currently, only
;; `emacs-lisp-mode' buffers are supported.
;;
;; The mode is quite simple, and is composed of two parts:
;;
;;; Abbrevs
;;
;; A large number of abbrevs which expand function
;; initials to their name.  A few examples:
;; 
;; - wcb -> with-current-buffer
;; - i -> insert
;; - r -> require '
;; - a -> and
;; 
;; However, these are defined in a way such that they ONLY expand in a
;; place where you would use a function, so hitting SPC after "(r"
;; expands to "(require '", but hitting SPC after "(delete-region r" will
;; NOT expand the `r', because that's obviously not a function.
;; Furtheromre, "#'r" will expand to "#'require" (note how it ommits that
;; extra quote, since it would be useless here).
;;
;;; Commands
;;
;; It also defines 4 commands, which really fit into this "follow the
;; thought-flow" way of writing.  The bindings are as follows, I
;; understand these don't fully adhere to conventions, and I'd
;; appreaciate suggestions on better bindings.
;; 
;; - M-RET :: Break line, and insert "()" with point in the middle.
;; - C-RET :: Do `forward-up-list', then do M-RET.
;; 
;; Hitting RET followed by a `(' was one of the most common key sequences
;; for me while writing elisp, so giving it a quick-to-hit key was a
;; significant improvement.
;; 
;; - C-c f :: Find function under point.  If it is not defined, create a
;; definition for it below the current function and leave point inside.
;; - C-c v :: Same, but for variable.
;; 
;; With these commands, you just write your code as you think of it.  Once
;; you hit a "stop-point" of sorts in your tought flow, you hit `C-c f/v`
;; on any undefined functions/variables, write their definitions, and hit
;; `C-u C-SPC` to go back to the main function.
;; 
;;; Small Example
;;
;; With the above (assuming you use something like paredit or
;; electric-pair-mode), if you write:
;;
;;   ( w t b M-RET i SPC text
;; 
;; You get
;; 
;;   (with-temp-buffer (insert text))

\f
;;; Code:
(eval-when-compile
  (require 'subr-x))

;;; Predicates
(defun sotlisp--auto-paired-p ()
  "Non-nil if this buffer auto-inserts parentheses."
  (or (bound-and-true-p electric-pair-mode)
      (bound-and-true-p paredit-mode)
      (bound-and-true-p smartparens-mode)))

(defun sotlisp--function-form-p ()
  "Non-nil if point is at the start of a sexp.
Specially, avoids matching inside argument lists."
  (and (eq (char-before) ?\()
       (not (looking-back "(\\(defun\\s-+.*\\|lambda\\s-+\\)("))
       (not (string-match (rx (syntax symbol)) (string last-command-event)))))

(defun sotlisp--function-quote-p ()
  "Non-nil if point is at a sharp-quote."
  (looking-back "#'"))

(defun sotlisp--function-p ()
  "Non-nil if point is at reasonable place for a function name.
Returns non-nil if, after moving backwards by a sexp, either
`sotlisp--function-form-p' or `sotlisp--function-quote-p' return
non-nil."
  (save-excursion
    (ignore-errors
      (skip-chars-backward (rx alnum))
      (or (sotlisp--function-form-p)
          (sotlisp--function-quote-p)))))

(defun sotlisp--whitespace-p ()
  "Non-nil if current `self-insert'ed char is whitespace."
  (ignore-errors
    (string-match (rx space) (string last-command-event))))

\f
;;; Expansion logic
(defvar sotlisp--needs-moving nil
  "Will `sotlisp--move-to-$' move point after insertion?")

(defun sotlisp--move-to-$ ()
  "Move backwards until `$' and delete it.
Point is left where the `$' char was.  Does nothing if variable
`sotlisp-mode' is nil."
  (when (bound-and-true-p speed-of-thought-mode)
    (when sotlisp--needs-moving
      (setq sotlisp--needs-moving nil)
      (skip-chars-backward "^\\$")
      (delete-char -1))))

(add-hook 'post-command-hook #'sotlisp--move-to-$ 'append)

(defun sotlisp--maybe-skip-closing-paren ()
  "Move past `)' if variable `electric-pair-mode' is enabled."
  (when (and (char-after ?\))
             (sotlisp--auto-paired-p))
    (forward-char 1)))

(defvar sotlisp--function-table (make-hash-table :test #'equal)
  "Table where function abbrev expansions are stored.")

(defun sotlisp--expand-function ()
  "Expand the function abbrev before point.
See `sotlisp-define-function-abbrev'."
  (let ((r (point)))
    (skip-chars-backward (rx alnum))
    (let* ((name (buffer-substring (point) r))
           (expansion (gethash name sotlisp--function-table)))
      (delete-region (point) r)
      (if (sotlisp--function-quote-p)
          ;; After #' use the simple expansion.
          (insert (sotlisp--simplify-function-expansion expansion))
        ;; Inside a form, use the full expansion.
        (insert expansion)
        (when (string-match "\\$" expansion)
          (setq sotlisp--needs-moving t))))
    ;; Inform `expand-abbrev' that `self-insert-command' should not
    ;; trigger, by returning non-nil on SPC.
    (when (sotlisp--whitespace-p)
      ;; And maybe move out of closing paren if expansion ends with $.
      (when (eq (char-before) ?$)
        (delete-char -1)
        (setq sotlisp--needs-moving nil)
        (sotlisp--maybe-skip-closing-paren))
      t)))

(put 'sotlisp--expand-function 'no-self-insert t)

(defun sotlisp--simplify-function-expansion (expansion)
  "Take a substring of EXPANSION up to first space.
The space char is not included.  Any \"$\" are also removed."
  (replace-regexp-in-string
   "\\$" ""
   (substring expansion 0 (string-match " " expansion))))

\f
;;; Abbrev definitions
(defconst sotlisp--default-function-abbrevs
  '(
    ("a" . "and ")
    ("ah" . "add-hook '")
    ("atl" . "add-to-list '")
    ("bb" . "bury-buffer")
    ("bc" . "forward-char -1")
    ("bfn" . "buffer-file-name")
    ("bn" . "buffer-name")
    ("bl" . "buffer-list$")
    ("bod" . "beginning-of-defun")
    ("bp" . "boundp '")
    ("bs" . "buffer-string$")
    ("bss" . "buffer-substring ")
    ("bw" . "forward-word -1")
    ("c" . "concat ")
    ("ca" . "char-after$")
    ("cc" . "condition-case er\n$\n(error nil)")
    ("ci" . "call-interactively ")
    ("cip" . "called-interactively-p 'any")
    ("csv" . "customize-save-variable '")
    ("d" . "delete-char 1")
    ("df" . "delete-file ")
    ("dl" . "dolist (it $)")
    ("dk" . "define-key ")
    ("dmp" . "derived-mode-p '")
    ("dr" . "delete-region ")
    ("e" . "error \"$\"")
    ("efn" . "expand-file-name ")
    ("f" . "format \"$\"")
    ("fb" . "fboundp '")
    ("fbp" . "fboundp '")
    ("fc" . "forward-char 1")
    ("ff" . "find-file ")
    ("fl" . "forward-line 1")
    ("fp" . "functionp ")
    ("frp" . "file-readable-p ")
    ("fs" . "forward-sexp 1")
    ("fw" . "forward-word 1")
    ("g" . "goto-char ")
    ("gc" . "goto-char ")
    ("gsk" . "global-set-key ")
    ("i" . "insert ")
    ("ie" . "ignore-errors ")
    ("k" . "kbd \"$\"")
    ("kb" . "kill-buffer")
    ("l" . "lambda ($)")
    ("la" . "looking-at \"$\"")
    ("lap" . "looking-at-p \"$\"")
    ("lb" . "looking-back \"$\"")
    ("let" . "let (($))")
    ("lp" . "listp ")
    ("m" . "message \"$%s\"")
    ("mb" . "match-beginning 0")
    ("me" . "match-end 0")
    ("ms" . "match-string 0")
    ("msnp" . "match-string-no-properties 0")
    ("n" . "not ")
    ("nl" . "forward-line 1")
    ("np" . "numberp ")
    ("ow" . "other-window 1")
    ("p" . "point$")
    ("pa" . "point-max$")
    ("pi" . "point-min$")
    ("r" . "require '")
    ("rh" . "remove-hook '")
    ("rm" . "replace-match \"$\"")
    ("rq" . "regexp-quote \"$\"")
    ("rris" . "replace-regexp-in-string ")
    ("rrs" . "replace-regexp-in-string ")
    ("rs" . "while (search-forward $ nil t)\n(replace-match \"\") nil t)")
    ("s" . "setq ")
    ("s=" . "string= ")
    ("sb" . "search-backward \"$\"")
    ("sbr" . "search-backward-regexp \"$\"")
    ("scb" . "skip-chars-backward \"$\r\n[:blank:]\"")
    ("scf" . "skip-chars-forward \"$\r\n[:blank:]\"")
    ("se" . "save-excursion")
    ("sf" . "search-forward \"$\"")
    ("sfr" . "search-forward-regexp \"$\"")
    ("sm" . "string-match \"$\"")
    ("smd" . "save-match-data")
    ("sn" . "symbol-name ")
    ("sp" . "stringp ")
    ("sr" . "save-restriction")
    ("ss" . "substring ")
    ("stb" . "switch-to-buffer ")
    ("sw" . "select-window ")
    ("tap" . "thing-at-point 'symbol")
    ("u" . "unless ")
    ("up" . "unwind-protect ")
    ("w" . "when ")
    ("wcb" . "with-current-buffer ")
    ("wf" . "write-file ")
    ("wh" . "while ")
    ("wl" . "window-list nil 'nominibuffer")
    ("wtb" . "with-temp-buffer")
    ("wtf" . "with-temp-file")
    )
  "Alist of (ABBREV . EXPANSION) used by `sotlisp'.")

(defun sotlisp-define-function-abbrev (name expansion)
  "Define a function abbrev expanding NAME to EXPANSION.
This abbrev will only be expanded in places where a function name is
sensible.  Roughly, this is right after a `(' or a `#''.

If EXPANSION is any string, it doesn't have to be the just the
name of a function.  In particular:
  - if it contains a `$', this char will not be inserted and
    point will be moved to its position after expansion.
  - if it contains a space, only a substring of it up to the
first space is inserted when expanding after a `#'' (this is done
by defining two different abbrevs).

For instance, if one defines
   (sotlisp-define-function-abbrev \"d\" \"delete-char 1\")

then triggering `expand-abbrev' after \"d\" expands in the
following way:
   (d    => (delete-char 1
   #'d   => #'delete-char"
  (define-abbrev emacs-lisp-mode-abbrev-table
    name t #'sotlisp--expand-function
    ;; Don't override user abbrevs
    :system t
    ;; Only expand in function places.
    :enable-function #'sotlisp--function-p)
  (puthash name expansion sotlisp--function-table))

(defun sotlisp-erase-all-abbrevs ()
  "Undefine all abbrevs defined by `sotlisp'."
  (interactive)
  (maphash (lambda (x _) (define-abbrev emacs-lisp-mode-abbrev-table x nil))
           sotlisp--function-table))

(defun sotlisp-define-all-abbrevs ()
  "Define all abbrevs in `sotlisp--default-function-abbrevs'."
  (interactive)
  (mapc (lambda (x) (sotlisp-define-function-abbrev (car x) (cdr x)))
    sotlisp--default-function-abbrevs))

\f
;;; The minor-mode
;;;###autoload
(define-minor-mode speed-of-thought-mode nil nil nil nil
  :global t
  (if speed-of-thought-mode
      (progn
        (add-hook 'emacs-lisp-mode-hook #'speed-of-thought--lisp-mode)
        (sotlisp-define-all-abbrevs)
        (mapc (lambda (b)
                (with-current-buffer b
                  (when (derived-mode-p 'emacs-lisp-mode)
                    (speed-of-thought--lisp-mode 1))))
          (buffer-list)))
    (remove-hook 'emacs-lisp-mode-hook #'speed-of-thought--lisp-mode)
    (sotlisp-erase-all-abbrevs)
    (mapc (lambda (b)
            (with-current-buffer b
              (when (derived-mode-p 'emacs-lisp-mode)
                (speed-of-thought--lisp-mode -1))))
      (buffer-list))))

(define-minor-mode speed-of-thought--lisp-mode nil nil " SoT"
  '(([M-return] . sotlisp-newline-and-parentheses)
    ([C-return] . sotlisp-downlist-newline-and-parentheses)
    ("\C-cf"    . sotlisp-find-or-define-function)
    ("\C-cv"    . sotlisp-find-or-define-variable)))

\f
;;; Commands
(defun sotlisp-newline-and-parentheses ()
  "`newline-and-indent' then insert a pair of parentheses."
  (interactive)
  (point)
  (ignore-errors
    (expand-abbrev))
  (newline-and-indent)
  (insert "()")
  (forward-char -1))

(defun sotlisp-downlist-newline-and-parentheses ()
  "`up-list', `newline-and-indent', then insert a parentheses pair."
  (interactive)
  (ignore-errors
    (expand-abbrev))
  (up-list)
  (newline-and-indent)
  (insert "()")
  (forward-char -1))

(defun sotlisp--find-in-buffer (r s)
  "Find the string (concat R (regexp-quote S)) somewhere in this buffer."
  (let ((l (save-excursion
             (goto-char (point-min))
             (save-match-data
               (when (search-forward-regexp (concat r (regexp-quote s) "\\_>")
                                            nil :noerror)
                 (match-beginning 0))))))
    (when l
      (push-mark)
      (goto-char l)
      l)))

(defun sotlisp-find-or-define-function (&optional prefix)
  "If symbol under point is a defined function, go to it, otherwise define it.
Essentially `find-function' on steroids.

If you write in your code the name of a function you haven't
defined yet, just place point on its name and hit \\[sotlisp-find-or-define-function]
and a defun will be inserted with point inside it.  After that,
you can just hit `pop-mark' to go back to where you were.
With a PREFIX argument, creates a `defmacro' instead.

If the function under point is already defined this just calls
`find-function', with one exception:
    if there's a defun (or equivalent) for this function in the
    current buffer, we go to that even if it's not where the
    global definition comes from (this is useful if you're
    writing an Emacs package that also happens to be installed
    through package.el).

With a prefix argument, defines a `defmacro' instead of a `defun'."
  (interactive "P")
  (let ((name (sotlisp--function-at-point)))
    (unless (and name (sotlisp--find-in-buffer "(def\\(un\\|macro\\|alias\\) " name))
      (let ((name-s (intern-soft name)))
        (if (fboundp name-s)
            (find-function name-s)
          (push-mark)
          (end-of-defun)
          (insert "\n(def" (if prefix "macro" "un")
                  " " name " (")
          (save-excursion (insert ")\n  \"\"\n  )\n")))))))

(defun sotlisp--function-at-point ()
  "Return name of `function-called-at-point'."
  (if (save-excursion
        (ignore-errors (forward-sexp -1)
                       (looking-at-p "#'")))
      (thing-at-point 'symbol)
    (if-let ((fcap (function-called-at-point)))
        (symbol-name fcap)
      (thing-at-point 'symbol))))

(defun sotlisp-find-or-define-variable (&optional prefix)
  "If symbol under point is a defined variable, go to it, otherwise define it.
Essentially `find-variable' on steroids.

If you write in your code the name of a variable you haven't
defined yet, place point on its name and hit \\[sotlisp-find-or-define-variable]
and a `defcustom' will be created with point inside.  After that,
you can just `pop-mark' to go back to where you were.  With a
PREFIX argument, creates a `defvar' instead.

If the variable under point is already defined this just calls
`find-variable', with one exception:
    if there's a defvar (or equivalent) for this variable in the
    current buffer, we go to that even if it's not where the
    global definition comes from (this is useful if you're
    writing an Emacs package that also happens to be installed
    through package.el).

With a prefix argument, defines a `defvar' instead of a `defcustom'."
  (interactive "P")
  (let ((name (symbol-name (variable-at-point t))))
    (unless (sotlisp--find-in-buffer "(def\\(custom\\|const\\|var\\) " name)
      (unless (and (symbolp (variable-at-point))
                   (ignore-errors (find-variable (variable-at-point)) t))
        (push-mark)
        (let ((name (thing-at-point 'symbol)))
          (beginning-of-defun)
          (when (looking-back "^;;;###autoload\\s-*\n")
            (forward-line -1))
          (insert "(def" (if prefix "var" "custom")
                  " " name " t")
          (save-excursion
            (insert "\n  \"\"\n  :type 'boolean)\n\n")))))))

(provide 'sotlisp)
;;; sotlisp.el ends here


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

end of thread, other threads:[~2015-01-27 18:29 UTC | newest]

Thread overview: 16+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2015-01-24 16:38 [Proposed Minor-mode] Speed of thought Lisp (was: Abbrevs for the most frequent elisp symbols) Artur Malabarba
2015-01-25 14:49 ` [Proposed Minor-mode] Speed of thought Lisp Stefan Monnier
2015-01-26 19:37   ` Artur Malabarba
2015-01-26 21:01     ` Dmitry Gutov
2015-01-26 23:46       ` Rasmus
2015-01-27  2:17         ` Dmitry Gutov
2015-01-27  3:36     ` Stefan Monnier
2015-01-27 12:28 ` Oleh Krehel
2015-01-27 13:57   ` Artur Malabarba
2015-01-27 14:15     ` Oleh Krehel
2015-01-27 15:25       ` Stefan Monnier
2015-01-27 15:40         ` Oleh Krehel
2015-01-27 16:40           ` Rasmus
2015-01-27 18:19             ` Oleh Krehel
2015-01-27 18:23               ` Dmitry Gutov
2015-01-27 18:29                 ` Oleh Krehel

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