--- /dev/null
+;;; sotlisp.el --- Write lisp at the speed of thought. -*- lexical-binding: t; -*-\r
+\r
+;; Copyright (C) 2014 Free Software Foundation, Inc.\r
+\r
+;; Author: Artur Malabarba <bruce.connor.am@gmail.com>\r
+;; Keywords: convenience, lisp\r
+;; Package-Requires: ((emacs "24.1"))\r
+\r
+;; This program is free software; you can redistribute it and/or modify\r
+;; it under the terms of the GNU General Public License as published by\r
+;; the Free Software Foundation, either version 3 of the License, or\r
+;; (at your option) any later version.\r
+\r
+;; This program is distributed in the hope that it will be useful,\r
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\r
+;; GNU General Public License for more details.\r
+\r
+;; You should have received a copy of the GNU General Public License\r
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.\r
+\r
+;;; Commentary:\r
+;;\r
+;; This defines a new global minor-mode `speed-of-thought-mode', which\r
+;; activates locally on any supported buffer. Currently, only\r
+;; `emacs-lisp-mode' buffers are supported.\r
+;;\r
+;; The mode is quite simple, and is composed of two parts:\r
+;;\r
+;;; Abbrevs\r
+;;\r
+;; A large number of abbrevs which expand function\r
+;; initials to their name. A few examples:\r
+;; \r
+;; - wcb -> with-current-buffer\r
+;; - i -> insert\r
+;; - r -> require '\r
+;; - a -> and\r
+;; \r
+;; However, these are defined in a way such that they ONLY expand in a\r
+;; place where you would use a function, so hitting SPC after "(r"\r
+;; expands to "(require '", but hitting SPC after "(delete-region r" will\r
+;; NOT expand the `r', because that's obviously not a function.\r
+;; Furtheromre, "#'r" will expand to "#'require" (note how it ommits that\r
+;; extra quote, since it would be useless here).\r
+;;\r
+;;; Commands\r
+;;\r
+;; It also defines 4 commands, which really fit into this "follow the\r
+;; thought-flow" way of writing. The bindings are as follows, I\r
+;; understand these don't fully adhere to conventions, and I'd\r
+;; appreaciate suggestions on better bindings.\r
+;; \r
+;; - M-RET :: Break line, and insert "()" with point in the middle.\r
+;; - C-RET :: Do `forward-up-list', then do M-RET.\r
+;; \r
+;; Hitting RET followed by a `(' was one of the most common key sequences\r
+;; for me while writing elisp, so giving it a quick-to-hit key was a\r
+;; significant improvement.\r
+;; \r
+;; - C-c f :: Find function under point. If it is not defined, create a\r
+;; definition for it below the current function and leave point inside.\r
+;; - C-c v :: Same, but for variable.\r
+;; \r
+;; With these commands, you just write your code as you think of it. Once\r
+;; you hit a "stop-point" of sorts in your tought flow, you hit `C-c f/v`\r
+;; on any undefined functions/variables, write their definitions, and hit\r
+;; `C-u C-SPC` to go back to the main function.\r
+;; \r
+;;; Small Example\r
+;;\r
+;; With the above (assuming you use something like paredit or\r
+;; electric-pair-mode), if you write:\r
+;;\r
+;; ( w t b M-RET i SPC text\r
+;; \r
+;; You get\r
+;; \r
+;; (with-temp-buffer (insert text))\r
+\r
+\f\r
+;;; Code:\r
+(eval-when-compile\r
+ (require 'subr-x))\r
+\r
+;;; Predicates\r
+(defun sotlisp--auto-paired-p ()\r
+ "Non-nil if this buffer auto-inserts parentheses."\r
+ (or (bound-and-true-p electric-pair-mode)\r
+ (bound-and-true-p paredit-mode)\r
+ (bound-and-true-p smartparens-mode)))\r
+\r
+(defun sotlisp--function-form-p ()\r
+ "Non-nil if point is at the start of a sexp.\r
+Specially, avoids matching inside argument lists."\r
+ (and (eq (char-before) ?\()\r
+ (not (looking-back "(\\(defun\\s-+.*\\|lambda\\s-+\\)("))\r
+ (not (string-match (rx (syntax symbol)) (string last-command-event)))))\r
+\r
+(defun sotlisp--function-quote-p ()\r
+ "Non-nil if point is at a sharp-quote."\r
+ (looking-back "#'"))\r
+\r
+(defun sotlisp--function-p ()\r
+ "Non-nil if point is at reasonable place for a function name.\r
+Returns non-nil if, after moving backwards by a sexp, either\r
+`sotlisp--function-form-p' or `sotlisp--function-quote-p' return\r
+non-nil."\r
+ (save-excursion\r
+ (ignore-errors\r
+ (skip-chars-backward (rx alnum))\r
+ (or (sotlisp--function-form-p)\r
+ (sotlisp--function-quote-p)))))\r
+\r
+(defun sotlisp--whitespace-p ()\r
+ "Non-nil if current `self-insert'ed char is whitespace."\r
+ (ignore-errors\r
+ (string-match (rx space) (string last-command-event))))\r
+\r
+\f\r
+;;; Expansion logic\r
+(defvar sotlisp--needs-moving nil\r
+ "Will `sotlisp--move-to-$' move point after insertion?")\r
+\r
+(defun sotlisp--move-to-$ ()\r
+ "Move backwards until `$' and delete it.\r
+Point is left where the `$' char was. Does nothing if variable\r
+`sotlisp-mode' is nil."\r
+ (when (bound-and-true-p speed-of-thought-mode)\r
+ (when sotlisp--needs-moving\r
+ (setq sotlisp--needs-moving nil)\r
+ (skip-chars-backward "^\\$")\r
+ (delete-char -1))))\r
+\r
+(add-hook 'post-command-hook #'sotlisp--move-to-$ 'append)\r
+\r
+(defun sotlisp--maybe-skip-closing-paren ()\r
+ "Move past `)' if variable `electric-pair-mode' is enabled."\r
+ (when (and (char-after ?\))\r
+ (sotlisp--auto-paired-p))\r
+ (forward-char 1)))\r
+\r
+(defvar sotlisp--function-table (make-hash-table :test #'equal)\r
+ "Table where function abbrev expansions are stored.")\r
+\r
+(defun sotlisp--expand-function ()\r
+ "Expand the function abbrev before point.\r
+See `sotlisp-define-function-abbrev'."\r
+ (let ((r (point)))\r
+ (skip-chars-backward (rx alnum))\r
+ (let* ((name (buffer-substring (point) r))\r
+ (expansion (gethash name sotlisp--function-table)))\r
+ (delete-region (point) r)\r
+ (if (sotlisp--function-quote-p)\r
+ ;; After #' use the simple expansion.\r
+ (insert (sotlisp--simplify-function-expansion expansion))\r
+ ;; Inside a form, use the full expansion.\r
+ (insert expansion)\r
+ (when (string-match "\\$" expansion)\r
+ (setq sotlisp--needs-moving t))))\r
+ ;; Inform `expand-abbrev' that `self-insert-command' should not\r
+ ;; trigger, by returning non-nil on SPC.\r
+ (when (sotlisp--whitespace-p)\r
+ ;; And maybe move out of closing paren if expansion ends with $.\r
+ (when (eq (char-before) ?$)\r
+ (delete-char -1)\r
+ (setq sotlisp--needs-moving nil)\r
+ (sotlisp--maybe-skip-closing-paren))\r
+ t)))\r
+\r
+(put 'sotlisp--expand-function 'no-self-insert t)\r
+\r
+(defun sotlisp--simplify-function-expansion (expansion)\r
+ "Take a substring of EXPANSION up to first space.\r
+The space char is not included. Any \"$\" are also removed."\r
+ (replace-regexp-in-string\r
+ "\\$" ""\r
+ (substring expansion 0 (string-match " " expansion))))\r
+\r
+\f\r
+;;; Abbrev definitions\r
+(defconst sotlisp--default-function-abbrevs\r
+ '(\r
+ ("a" . "and ")\r
+ ("ah" . "add-hook '")\r
+ ("atl" . "add-to-list '")\r
+ ("bb" . "bury-buffer")\r
+ ("bc" . "forward-char -1")\r
+ ("bfn" . "buffer-file-name")\r
+ ("bn" . "buffer-name")\r
+ ("bl" . "buffer-list$")\r
+ ("bod" . "beginning-of-defun")\r
+ ("bp" . "boundp '")\r
+ ("bs" . "buffer-string$")\r
+ ("bss" . "buffer-substring ")\r
+ ("bw" . "forward-word -1")\r
+ ("c" . "concat ")\r
+ ("ca" . "char-after$")\r
+ ("cc" . "condition-case er\n$\n(error nil)")\r
+ ("ci" . "call-interactively ")\r
+ ("cip" . "called-interactively-p 'any")\r
+ ("csv" . "customize-save-variable '")\r
+ ("d" . "delete-char 1")\r
+ ("df" . "delete-file ")\r
+ ("dl" . "dolist (it $)")\r
+ ("dk" . "define-key ")\r
+ ("dmp" . "derived-mode-p '")\r
+ ("dr" . "delete-region ")\r
+ ("e" . "error \"$\"")\r
+ ("efn" . "expand-file-name ")\r
+ ("f" . "format \"$\"")\r
+ ("fb" . "fboundp '")\r
+ ("fbp" . "fboundp '")\r
+ ("fc" . "forward-char 1")\r
+ ("ff" . "find-file ")\r
+ ("fl" . "forward-line 1")\r
+ ("fp" . "functionp ")\r
+ ("frp" . "file-readable-p ")\r
+ ("fs" . "forward-sexp 1")\r
+ ("fw" . "forward-word 1")\r
+ ("g" . "goto-char ")\r
+ ("gc" . "goto-char ")\r
+ ("gsk" . "global-set-key ")\r
+ ("i" . "insert ")\r
+ ("ie" . "ignore-errors ")\r
+ ("k" . "kbd \"$\"")\r
+ ("kb" . "kill-buffer")\r
+ ("l" . "lambda ($)")\r
+ ("la" . "looking-at \"$\"")\r
+ ("lap" . "looking-at-p \"$\"")\r
+ ("lb" . "looking-back \"$\"")\r
+ ("let" . "let (($))")\r
+ ("lp" . "listp ")\r
+ ("m" . "message \"$%s\"")\r
+ ("mb" . "match-beginning 0")\r
+ ("me" . "match-end 0")\r
+ ("ms" . "match-string 0")\r
+ ("msnp" . "match-string-no-properties 0")\r
+ ("n" . "not ")\r
+ ("nl" . "forward-line 1")\r
+ ("np" . "numberp ")\r
+ ("ow" . "other-window 1")\r
+ ("p" . "point$")\r
+ ("pa" . "point-max$")\r
+ ("pi" . "point-min$")\r
+ ("r" . "require '")\r
+ ("rh" . "remove-hook '")\r
+ ("rm" . "replace-match \"$\"")\r
+ ("rq" . "regexp-quote \"$\"")\r
+ ("rris" . "replace-regexp-in-string ")\r
+ ("rrs" . "replace-regexp-in-string ")\r
+ ("rs" . "while (search-forward $ nil t)\n(replace-match \"\") nil t)")\r
+ ("s" . "setq ")\r
+ ("s=" . "string= ")\r
+ ("sb" . "search-backward \"$\"")\r
+ ("sbr" . "search-backward-regexp \"$\"")\r
+ ("scb" . "skip-chars-backward \"$\r\n[:blank:]\"")\r
+ ("scf" . "skip-chars-forward \"$\r\n[:blank:]\"")\r
+ ("se" . "save-excursion")\r
+ ("sf" . "search-forward \"$\"")\r
+ ("sfr" . "search-forward-regexp \"$\"")\r
+ ("sm" . "string-match \"$\"")\r
+ ("smd" . "save-match-data")\r
+ ("sn" . "symbol-name ")\r
+ ("sp" . "stringp ")\r
+ ("sr" . "save-restriction")\r
+ ("ss" . "substring ")\r
+ ("stb" . "switch-to-buffer ")\r
+ ("sw" . "select-window ")\r
+ ("tap" . "thing-at-point 'symbol")\r
+ ("u" . "unless ")\r
+ ("up" . "unwind-protect ")\r
+ ("w" . "when ")\r
+ ("wcb" . "with-current-buffer ")\r
+ ("wf" . "write-file ")\r
+ ("wh" . "while ")\r
+ ("wl" . "window-list nil 'nominibuffer")\r
+ ("wtb" . "with-temp-buffer")\r
+ ("wtf" . "with-temp-file")\r
+ )\r
+ "Alist of (ABBREV . EXPANSION) used by `sotlisp'.")\r
+\r
+(defun sotlisp-define-function-abbrev (name expansion)\r
+ "Define a function abbrev expanding NAME to EXPANSION.\r
+This abbrev will only be expanded in places where a function name is\r
+sensible. Roughly, this is right after a `(' or a `#''.\r
+\r
+If EXPANSION is any string, it doesn't have to be the just the\r
+name of a function. In particular:\r
+ - if it contains a `$', this char will not be inserted and\r
+ point will be moved to its position after expansion.\r
+ - if it contains a space, only a substring of it up to the\r
+first space is inserted when expanding after a `#'' (this is done\r
+by defining two different abbrevs).\r
+\r
+For instance, if one defines\r
+ (sotlisp-define-function-abbrev \"d\" \"delete-char 1\")\r
+\r
+then triggering `expand-abbrev' after \"d\" expands in the\r
+following way:\r
+ (d => (delete-char 1\r
+ #'d => #'delete-char"\r
+ (define-abbrev emacs-lisp-mode-abbrev-table\r
+ name t #'sotlisp--expand-function\r
+ ;; Don't override user abbrevs\r
+ :system t\r
+ ;; Only expand in function places.\r
+ :enable-function #'sotlisp--function-p)\r
+ (puthash name expansion sotlisp--function-table))\r
+\r
+(defun sotlisp-erase-all-abbrevs ()\r
+ "Undefine all abbrevs defined by `sotlisp'."\r
+ (interactive)\r
+ (maphash (lambda (x _) (define-abbrev emacs-lisp-mode-abbrev-table x nil))\r
+ sotlisp--function-table))\r
+\r
+(defun sotlisp-define-all-abbrevs ()\r
+ "Define all abbrevs in `sotlisp--default-function-abbrevs'."\r
+ (interactive)\r
+ (mapc (lambda (x) (sotlisp-define-function-abbrev (car x) (cdr x)))\r
+ sotlisp--default-function-abbrevs))\r
+\r
+\f\r
+;;; The global minor-mode\r
+(defvar speed-of-thought-turn-on-hook '(sotlisp-turn-on-everywhere)\r
+ "Hook run once when `speed-of-thought-mode' is enabled.\r
+Note that `speed-of-thought-mode' is global, so this is not run\r
+on every buffer.\r
+\r
+See `sotlisp-turn-on-everywhere' for an example of what a\r
+function in this hook should do.")\r
+\r
+(defvar speed-of-thought-turn-off-hook '(sotlisp-turn-off-everywhere)\r
+ "Hook run once when `speed-of-thought-mode' is disabled.\r
+Note that `speed-of-thought-mode' is global, so this is not run\r
+on every buffer.\r
+\r
+See `sotlisp-turn-on-everywhere' for an example of what a\r
+function in this hook should do.")\r
+\r
+;;;###autoload\r
+(define-minor-mode speed-of-thought-mode nil nil nil nil\r
+ :global t\r
+ (run-hooks (if speed-of-thought-mode\r
+ 'speed-of-thought-turn-on-hook\r
+ 'speed-of-thought-turn-off-hook)))\r
+\r
+\f\r
+;;; The local minor-mode\r
+(defun sotlisp-turn-on-everywhere ()\r
+ "Call-once function to turn on sotlisp everywhere.\r
+Calls `sotlisp-mode' on all `emacs-lisp-mode' buffers, and sets\r
+up a hook and abbrevs."\r
+ (add-hook 'emacs-lisp-mode-hook #'sotlisp-mode)\r
+ (sotlisp-define-all-abbrevs)\r
+ (mapc (lambda (b)\r
+ (with-current-buffer b\r
+ (when (derived-mode-p 'emacs-lisp-mode)\r
+ (sotlisp-mode 1))))\r
+ (buffer-list)))\r
+\r
+(defun sotlisp-turn-off-everywhere ()\r
+ "Call-once function to turn off sotlisp everywhere.\r
+Removes `sotlisp-mode' from all `emacs-lisp-mode' buffers, and\r
+removes hooks and abbrevs."\r
+ (remove-hook 'emacs-lisp-mode-hook #'sotlisp-mode)\r
+ (sotlisp-erase-all-abbrevs)\r
+ (mapc (lambda (b)\r
+ (with-current-buffer b\r
+ (when (derived-mode-p 'emacs-lisp-mode)\r
+ (sotlisp-mode -1))))\r
+ (buffer-list)))\r
+\r
+(define-minor-mode sotlisp-mode nil nil " SoT"\r
+ '(([M-return] . sotlisp-newline-and-parentheses)\r
+ ([C-return] . sotlisp-downlist-newline-and-parentheses)\r
+ ("\C-cf" . sotlisp-find-or-define-function)\r
+ ("\C-cv" . sotlisp-find-or-define-variable)))\r
+\r
+\f\r
+;;; Commands\r
+(defun sotlisp-newline-and-parentheses ()\r
+ "`newline-and-indent' then insert a pair of parentheses."\r
+ (interactive)\r
+ (point)\r
+ (ignore-errors (expand-abbrev))\r
+ (newline-and-indent)\r
+ (insert "()")\r
+ (forward-char -1))\r
+\r
+(defun sotlisp-downlist-newline-and-parentheses ()\r
+ "`up-list', `newline-and-indent', then insert a parentheses pair."\r
+ (interactive)\r
+ (ignore-errors (expand-abbrev))\r
+ (up-list)\r
+ (newline-and-indent)\r
+ (insert "()")\r
+ (forward-char -1))\r
+\r
+(defun sotlisp--find-in-buffer (r s)\r
+ "Find the string (concat R (regexp-quote S)) somewhere in this buffer."\r
+ (let ((l (save-excursion\r
+ (goto-char (point-min))\r
+ (save-match-data\r
+ (when (search-forward-regexp (concat r (regexp-quote s) "\\_>")\r
+ nil :noerror)\r
+ (match-beginning 0))))))\r
+ (when l\r
+ (push-mark)\r
+ (goto-char l)\r
+ l)))\r
+\r
+(defun sotlisp--beginning-of-defun ()\r
+ "`push-mark' and move above this defun."\r
+ (push-mark)\r
+ (beginning-of-defun)\r
+ (when (looking-back "^;;;###autoload\\s-*\n")\r
+ (forward-line -1)))\r
+\r
+(defun sotlisp--function-at-point ()\r
+ "Return name of `function-called-at-point'."\r
+ (if (save-excursion\r
+ (ignore-errors (forward-sexp -1)\r
+ (looking-at-p "#'")))\r
+ (thing-at-point 'symbol)\r
+ (if-let ((fcap (function-called-at-point)))\r
+ (symbol-name fcap)\r
+ (thing-at-point 'symbol))))\r
+\r
+(defun sotlisp-find-or-define-function (&optional prefix)\r
+ "If symbol under point is a defined function, go to it, otherwise define it.\r
+Essentially `find-function' on steroids.\r
+\r
+If you write in your code the name of a function you haven't\r
+defined yet, just place point on its name and hit \\[sotlisp-find-or-define-function]\r
+and a defun will be inserted with point inside it. After that,\r
+you can just hit `pop-mark' to go back to where you were.\r
+With a PREFIX argument, creates a `defmacro' instead.\r
+\r
+If the function under point is already defined this just calls\r
+`find-function', with one exception:\r
+ if there's a defun (or equivalent) for this function in the\r
+ current buffer, we go to that even if it's not where the\r
+ global definition comes from (this is useful if you're\r
+ writing an Emacs package that also happens to be installed\r
+ through package.el).\r
+\r
+With a prefix argument, defines a `defmacro' instead of a `defun'."\r
+ (interactive "P")\r
+ (let ((name (sotlisp--function-at-point)))\r
+ (unless (and name (sotlisp--find-in-buffer "(def\\(un\\|macro\\|alias\\) " name))\r
+ (let ((name-s (intern-soft name)))\r
+ (if (fboundp name-s)\r
+ (find-function name-s)\r
+ (sotlisp--beginning-of-defun)\r
+ (insert "(def" (if prefix "macro" "un")\r
+ " " name " (")\r
+ (save-excursion (insert ")\n \"\"\n )\n\n")))))))\r
+\r
+(defun sotlisp-find-or-define-variable (&optional prefix)\r
+ "If symbol under point is a defined variable, go to it, otherwise define it.\r
+Essentially `find-variable' on steroids.\r
+\r
+If you write in your code the name of a variable you haven't\r
+defined yet, place point on its name and hit \\[sotlisp-find-or-define-variable]\r
+and a `defcustom' will be created with point inside. After that,\r
+you can just `pop-mark' to go back to where you were. With a\r
+PREFIX argument, creates a `defvar' instead.\r
+\r
+If the variable under point is already defined this just calls\r
+`find-variable', with one exception:\r
+ if there's a defvar (or equivalent) for this variable in the\r
+ current buffer, we go to that even if it's not where the\r
+ global definition comes from (this is useful if you're\r
+ writing an Emacs package that also happens to be installed\r
+ through package.el).\r
+\r
+With a prefix argument, defines a `defvar' instead of a `defcustom'."\r
+ (interactive "P")\r
+ (let ((name (symbol-name (variable-at-point t))))\r
+ (unless (sotlisp--find-in-buffer "(def\\(custom\\|const\\|var\\) " name)\r
+ (unless (and (symbolp (variable-at-point))\r
+ (ignore-errors (find-variable (variable-at-point)) t))\r
+ (let ((name (thing-at-point 'symbol)))\r
+ (sotlisp--beginning-of-defun)\r
+ (insert "(def" (if prefix "var" "custom")\r
+ " " name " t")\r
+ (save-excursion\r
+ (insert "\n \"\""\r
+ (if prefix "" "\n :type 'boolean")\r
+ ")\n\n")))))))\r
+\r
+(provide 'sotlisp)\r
+;;; sotlisp.el ends here\r
+\r