├── .github └── FUNDING.yml ├── images ├── doc-clojure.png ├── doc-elisp.png ├── lispy-logo.png ├── arglist-elisp.png └── arglist-clojure.png ├── .gitignore ├── Cookbook.py ├── mypy.ini ├── lispy-pkg.el ├── .dir-locals.el ├── targets ├── checkdoc.el ├── compile.el ├── tlc.clj ├── interactive-init.el ├── check-declare.el └── install-deps.el ├── deps.edn ├── .travis.yml ├── doc ├── develop.org └── clojure.org ├── Makefile ├── elpa.el ├── le-js.el ├── le-racket.el ├── lispy-clojure.cljs ├── le-julia.el ├── le-scheme.el ├── le-hy.el ├── le-lisp.el ├── lispy-clojure-test.clj ├── test └── test_lispy-python.py ├── lispy-occur.el ├── lispy-tags.el ├── lispytutor └── lispytutor.el ├── lispy-inline.el ├── lispy-clojure.clj ├── le-clojure.el ├── README.md └── lispy-python.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | liberapay: abo-abo 2 | patreon: abo_abo 3 | -------------------------------------------------------------------------------- /images/doc-clojure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abo-abo/lispy/HEAD/images/doc-clojure.png -------------------------------------------------------------------------------- /images/doc-elisp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abo-abo/lispy/HEAD/images/doc-elisp.png -------------------------------------------------------------------------------- /images/lispy-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abo-abo/lispy/HEAD/images/lispy-logo.png -------------------------------------------------------------------------------- /images/arglist-elisp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abo-abo/lispy/HEAD/images/arglist-elisp.png -------------------------------------------------------------------------------- /images/arglist-clojure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abo-abo/lispy/HEAD/images/arglist-clojure.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /init.el 2 | /gh-pages/ 3 | /.projectile 4 | /.cask/ 5 | *.elc 6 | \#*# 7 | .#* 8 | *~ 9 | *$py.class -------------------------------------------------------------------------------- /Cookbook.py: -------------------------------------------------------------------------------- 1 | def test(recipe): 2 | return ["pytest test/test_lispy-python.py"] 3 | 4 | def typecheck(recipe): 5 | return "dmypy run -- lispy-python.py" 6 | 7 | # del typecheck 8 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | # disallow_untyped_defs = True 3 | color_output = False 4 | 5 | [mypy-jedi.*] 6 | ignore_missing_imports = True 7 | 8 | [mypy-__builtin__] 9 | ignore_missing_imports = True -------------------------------------------------------------------------------- /lispy-pkg.el: -------------------------------------------------------------------------------- 1 | (define-package "lispy" "0.27.0" 2 | "vi-like Paredit" 3 | '((emacs "24.3") 4 | (ace-window "0.9.0") 5 | (iedit "0.9.9") 6 | (swiper "0.13.4") 7 | (hydra "0.14.0") 8 | (zoutline "0.2.0"))) 9 | -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ;;; Directory Local Variables 2 | ;;; For more information see (info "(emacs) Directory Variables") 3 | 4 | ((emacs-lisp-mode 5 | (bug-reference-url-format . "https://github.com/abo-abo/lispy/issues/%s") 6 | (indent-tabs-mode . nil))) 7 | -------------------------------------------------------------------------------- /targets/checkdoc.el: -------------------------------------------------------------------------------- 1 | (setq files '("lispy.el" 2 | "lispy-inline.el" 3 | "le-clojure.el" 4 | "le-scheme.el" 5 | "le-lisp.el")) 6 | (require 'checkdoc) 7 | (dolist (file files) 8 | (checkdoc-file file)) 9 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.9.0"} 2 | ;; get rid of weird warnings on startup 3 | org.slf4j/slf4j-simple {:mvn/version "1.6.2"} 4 | com.cemerick/pomegranate {:mvn/version "0.4.0"}} 5 | :aliases 6 | {:test 7 | {:extra-paths ["."]}}} 8 | -------------------------------------------------------------------------------- /targets/compile.el: -------------------------------------------------------------------------------- 1 | (setq files '("lispy.el" 2 | "lispy-inline.el" 3 | "le-clojure.el" 4 | "le-scheme.el" 5 | "le-lisp.el" 6 | "le-python.el" 7 | "le-racket.el")) 8 | (setq byte-compile--use-old-handlers nil) 9 | (mapc #'byte-compile-file files) 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | sudo: required 3 | language: emacs-lisp 4 | env: 5 | matrix: 6 | - emacs=emacs25 7 | - emacs=emacs-snapshot 8 | 9 | before_install: 10 | - sudo add-apt-repository -y ppa:kelleyk/emacs 11 | - sudo add-apt-repository -y ppa:ubuntu-elisp 12 | - sudo apt-get update -qq 13 | - sudo apt-get install -qq $emacs 14 | 15 | script: 16 | - make update 17 | - make test 18 | -------------------------------------------------------------------------------- /doc/develop.org: -------------------------------------------------------------------------------- 1 | * Introduction 2 | ** Installing dependencies 3 | Run this to install/upgrade dependecies from MELPA. 4 | #+begin_src sh 5 | make update 6 | #+end_src 7 | 8 | To use MELPA-Stable, adjust the command: 9 | #+begin_src sh 10 | MELPA_STABLE=1 make update 11 | #+end_src 12 | 13 | ** Running tests 14 | #+begin_src sh 15 | make test 16 | #+end_src 17 | 18 | ** Running interactively 19 | #+begin_src sh 20 | make elisp 21 | #+end_src 22 | 23 | or: 24 | #+begin_src sh 25 | MELPA_STABLE=1 make elisp 26 | #+end_src 27 | -------------------------------------------------------------------------------- /targets/tlc.clj: -------------------------------------------------------------------------------- 1 | (defn disable-illegal-access-warnings [] 2 | (let [the-unsafe (. (Class/forName "sun.misc.Unsafe") getDeclaredField "theUnsafe") 3 | u (do 4 | (. the-unsafe setAccessible true) 5 | (. the-unsafe get nil)) 6 | cls (Class/forName "jdk.internal.module.IllegalAccessLogger") 7 | logger (. cls getDeclaredField "logger")] 8 | (. u putObjectVolatile cls (. u staticFieldOffset logger) nil))) 9 | 10 | (defn print-versions [] 11 | (println (str "Clojure: " (clojure-version))) 12 | (println (str "JVM: " (System/getProperty "java.version")))) 13 | 14 | (print-versions) 15 | (disable-illegal-access-warnings) 16 | (load-file "lispy-clojure.clj") 17 | (load-file "lispy-clojure-test.clj") 18 | -------------------------------------------------------------------------------- /targets/interactive-init.el: -------------------------------------------------------------------------------- 1 | ;;* Base 2 | (require 'lispy) 3 | (dolist (h '(emacs-lisp-mode-hook 4 | lisp-interaction-mode-hook 5 | clojure-mode-hook 6 | scheme-mode-hook 7 | lisp-mode-hook)) 8 | (add-hook h #'lispy-mode)) 9 | (ivy-mode) 10 | (defadvice save-buffers-kill-emacs (around no-query-kill-emacs activate) 11 | "Prevent annoying \"Active processes exist\" query when you quit Emacs." 12 | (lispy-flet (process-list ()) ad-do-it)) 13 | 14 | ;;* Common Lisp 15 | (setq inferior-lisp-program "sbcl") 16 | (setq slime-contribs '(slime-fancy)) 17 | (setq lispy-use-sly nil) 18 | ;; SLIME and SLY modify this hook even before they're required. Yuck. 19 | (setq lisp-mode-hook '(lispy-mode)) 20 | 21 | ;;* Clojure 22 | (require 'clojure-semantic nil t) 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | emacs ?= emacs 2 | BEMACS = $(emacs) -batch -l elpa.el 3 | LOAD = -l lispy-inline.el -l lispy.el 4 | QEMACS = $(emacs) -Q -l elpa.el -l targets/interactive-init.el 5 | 6 | all: compile test 7 | 8 | update: 9 | $(emacs) -batch -l targets/install-deps.el 10 | 11 | compile: 12 | $(BEMACS) $(LOAD) -l targets/compile.el 13 | 14 | checkdoc: 15 | $(emacs) -batch -l elpa.el $(LOAD) -l targets/checkdoc.el 16 | 17 | check-declare: 18 | $(BEMACS) $(LOAD) -l targets/check-declare.el 19 | 20 | test: 21 | @echo "Using $(shell which $(emacs))..." 22 | $(BEMACS) -l lispy-test.el $(LOAD) -f ert-run-tests-batch-and-exit 23 | 24 | plain: 25 | $(QEMACS) -l elpa.el lispy.el 26 | 27 | clojure: 28 | clojure -M -e '(load-file "targets/tlc.clj")' 29 | 30 | clean: 31 | rm -f *.elc 32 | 33 | .PHONY: all clean elisp check-declare test 34 | -------------------------------------------------------------------------------- /targets/check-declare.el: -------------------------------------------------------------------------------- 1 | (require 'check-declare) 2 | (setq check-declare-ext-errors t) 3 | (setq files '("lispy.el" 4 | "lispy-inline.el" 5 | "le-clojure.el" 6 | "le-scheme.el" 7 | "le-lisp.el")) 8 | (add-to-list 'load-path 9 | (concat (file-name-directory 10 | (locate-library "slime")) 11 | "contrib/")) 12 | (require 'slime-repl) 13 | 14 | (defun find-library-file (lib) 15 | (save-window-excursion 16 | (let ((buf (find-library lib))) 17 | (when buf 18 | (buffer-file-name buf))))) 19 | 20 | 21 | (let ((sly-file (find-library-file "sly"))) 22 | (when sly-file 23 | (add-to-list 24 | 'load-path 25 | (expand-file-name 26 | "contrib" (file-name-directory sly-file))))) 27 | 28 | (apply #'check-declare-files files) 29 | -------------------------------------------------------------------------------- /elpa.el: -------------------------------------------------------------------------------- 1 | ;; (setq package-user-dir 2 | ;; (expand-file-name 3 | ;; (format "~/.elpa/%s/elpa" 4 | ;; (concat emacs-version (when (getenv "MELPA_STABLE") "-stable"))))) 5 | ;; (package-initialize) 6 | ;; (setq package-archives 7 | ;; (list (if (getenv "MELPA_STABLE") 8 | ;; '("melpa-stable" . "https://stable.melpa.org/packages/") 9 | ;; '("melpa" . "http://melpa.org/packages/")) 10 | ;; '("gnu" . "http://elpa.gnu.org/packages/"))) 11 | 12 | 13 | (add-to-list 'load-path default-directory) 14 | 15 | ;; Silence the loading message 16 | (setq iedit-toggle-key-default nil) 17 | 18 | (defun straight-reload-all () 19 | (interactive) 20 | (let ((build-dir (expand-file-name "straight/build/" user-emacs-directory))) 21 | (dolist (pkg (delete "cl-lib" (delete ".." (delete "." (directory-files build-dir))))) 22 | (let* ((dir (expand-file-name pkg build-dir)) 23 | (autoloads (car (directory-files dir t "-autoloads.el")))) 24 | (add-to-list 'load-path dir) 25 | (when autoloads 26 | (load autoloads t 'nomessage)))))) 27 | (straight-reload-all) 28 | (message "load-path: %S" load-path) 29 | -------------------------------------------------------------------------------- /le-js.el: -------------------------------------------------------------------------------- 1 | (require 'indium) 2 | 3 | (defun lispy--eval-js (str) 4 | (let ((r nil)) 5 | (indium-eval 6 | str (lambda (value) (setq r (indium-render-remote-object-to-string value)))) 7 | (while (not r) 8 | (accept-process-output)) 9 | (substring-no-properties r))) 10 | 11 | (defun lispy--eval-js-str () 12 | (if (region-active-p) 13 | (lispy--string-dwim) 14 | (lispy--string-dwim 15 | (lispy-bounds-python-block)))) 16 | 17 | (defun lispy--js-completion-at-point () 18 | (let* ((prefix (buffer-substring-no-properties 19 | (let ((bol (point-at-bol)) 20 | (prev-delimiter (1+ (save-excursion 21 | (re-search-backward "[([:space:]]" nil t))))) 22 | (if prev-delimiter 23 | (max bol prev-delimiter) 24 | bol)) 25 | (point))) 26 | (expression (if (string-match-p "\\." prefix) 27 | (replace-regexp-in-string "\\.[^\\.]*$" "" prefix) 28 | "this")) 29 | (cands nil)) 30 | (indium-client-get-completion 31 | expression 32 | indium-debugger-current-frame 33 | (lambda (candidates) 34 | (setq cands candidates))) 35 | (while (null cands) 36 | (accept-process-output)) 37 | (list (- (point) (length (company-grab-symbol))) 38 | (point) 39 | (mapcar #'identity cands)))) 40 | 41 | (provide 'le-js) 42 | -------------------------------------------------------------------------------- /le-racket.el: -------------------------------------------------------------------------------- 1 | ;;; le-racket.el --- lispy support for Racket. -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2014-2019 Oleh Krehel 4 | 5 | ;; This file is not part of GNU Emacs 6 | 7 | ;; This file is free software; you can redistribute it and/or modify 8 | ;; it under the terms of the GNU General Public License as published by 9 | ;; the Free Software Foundation; either version 3, or (at your option) 10 | ;; any later version. 11 | 12 | ;; This program is distributed in the hope that it will be useful, 13 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ;; GNU General Public License for more details. 16 | 17 | ;; For a full copy of the GNU General Public License 18 | ;; see . 19 | 20 | ;;; Commentary: 21 | ;; 22 | 23 | ;;; Code: 24 | 25 | (require 'racket-mode nil t) 26 | 27 | (declare-function racket--cmd/async "ext:racket-repl") 28 | (declare-function racket--repl-session-id "ext:racket-repl") 29 | 30 | (defun lispy-goto-symbol-racket (symbol) 31 | "Go to the definition of the SYMBOL." 32 | (xref-find-definitions symbol)) 33 | 34 | (defun lispy--eval-racket (str) 35 | "Evaluate STR in the context of the current racket repl session." 36 | (let* ((awaiting 'RACKET-REPL-AWAITING) 37 | (response awaiting)) 38 | (racket--cmd/async (racket--repl-session-id) 39 | `(eval ,str) 40 | (lambda (v) 41 | (setq response v))) 42 | (with-timeout (1 43 | (error "racket-command process timeout")) 44 | (while (eq response awaiting) 45 | (accept-process-output nil 0.001)) 46 | response))) 47 | 48 | (defun lispy-eval-racket () 49 | (lispy--eval-racket (lispy--string-dwim))) 50 | 51 | (provide 'le-racket) 52 | 53 | ;;; le-racket.el ends here 54 | -------------------------------------------------------------------------------- /lispy-clojure.cljs: -------------------------------------------------------------------------------- 1 | ;;; lispy-clojure.cljs --- lispy support for ClojureScript. 2 | 3 | ;; Copyright (C) 2020 Oleh Krehel 4 | 5 | ;; This file is not part of GNU Emacs 6 | 7 | ;; This file is free software; you can redistribute it and/or modify 8 | ;; it under the terms of the GNU General Public License as published by 9 | ;; the Free Software Foundation; either version 3, or (at your option) 10 | ;; any later version. 11 | 12 | ;; This program is distributed in the hope that it will be useful, 13 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ;; GNU General Public License for more details. 16 | 17 | ;; For a full copy of the GNU General Public License 18 | ;; see . 19 | 20 | (ns lispy.clojure 21 | (:require 22 | [goog.object])) 23 | 24 | (defn object-members [obj] 25 | (let [obj (if (string? obj) 26 | (js/Object.getPrototypeOf obj) 27 | obj)] 28 | (loop [o obj 29 | res ()] 30 | (if (nil? o) 31 | (into () (set res)) 32 | (recur 33 | (js/Object.getPrototypeOf o) 34 | (concat 35 | res 36 | (map 37 | (fn [n] 38 | (let [tp (type (goog.object/get obj n))] 39 | (if (= tp js/Function) 40 | n 41 | (str "-" n)))) 42 | (remove 43 | (fn [x] (or (re-find #"__|constructor|[$]" x) 44 | (re-matches #"[0-9]+" x))) 45 | (vec (js/Object.getOwnPropertyNames o)))))))))) 46 | 47 | (defn shadow-map [] 48 | (or 49 | (aget js/window "shadows") 50 | (set! (. js/window -shadows) {}))) 51 | 52 | (defn shadow-unmap [nspc] 53 | (set! (. js/window -shadows) {})) 54 | 55 | (defn shadow-def 56 | "Give SYM in *ns* shadow value EXPR." 57 | [sym expr] 58 | (set! (. js/window -shadows) 59 | (assoc (shadow-map) (name sym) expr)) 60 | expr) 61 | -------------------------------------------------------------------------------- /targets/install-deps.el: -------------------------------------------------------------------------------- 1 | (defconst lispy-dev-packages 2 | '(iedit 3 | multiple-cursors 4 | cider 5 | company 6 | spiral 7 | slime 8 | sly 9 | geiser 10 | clojure-mode 11 | swiper 12 | hydra 13 | ace-window 14 | helm 15 | projectile 16 | find-file-in-project 17 | undercover 18 | zoutline)) 19 | 20 | (defun package-install-packages (packages) 21 | (setq melpa-stable (getenv "MELPA_STABLE")) 22 | (setq package-user-dir 23 | (expand-file-name 24 | (format "~/.elpa/%s/elpa" 25 | (concat emacs-version (when melpa-stable "-stable"))))) 26 | (message "installing in %s ...\n" package-user-dir) 27 | (package-initialize) 28 | (setq package-archives 29 | (list (if melpa-stable 30 | '("melpa-stable" . "https://stable.melpa.org/packages/") 31 | '("melpa" . "http://melpa.org/packages/")) 32 | '("gnu" . "http://elpa.gnu.org/packages/"))) 33 | (package-refresh-contents) 34 | 35 | (dolist (package packages) 36 | (if (package-installed-p package) 37 | (message "%S: OK" package) 38 | (condition-case nil 39 | (progn 40 | (package-install package) 41 | (message "%S: OK" package)) 42 | (error 43 | (message "%S: FAIL" package))))) 44 | 45 | (save-window-excursion 46 | (package-list-packages t) 47 | (condition-case nil 48 | (progn 49 | (package-menu-mark-upgrades) 50 | (package-menu-execute t)) 51 | (error 52 | (message "All packages up to date"))))) 53 | 54 | (defun straight-install-packages (packages) 55 | (defvar bootstrap-version) 56 | (let ((bootstrap-file 57 | (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory)) 58 | (bootstrap-version 5)) 59 | (message "user-emacs-directory: %S" user-emacs-directory) 60 | (unless (file-exists-p bootstrap-file) 61 | (with-current-buffer 62 | (url-retrieve-synchronously 63 | "https://raw.githubusercontent.com/raxod502/straight.el/develop/install.el" 64 | 'silent 'inhibit-cookies) 65 | (goto-char (point-max)) 66 | (eval-print-last-sexp))) 67 | (load bootstrap-file nil 'nomessage)) 68 | 69 | (dolist (package packages) 70 | (straight-use-package package))) 71 | 72 | (straight-install-packages lispy-dev-packages) 73 | -------------------------------------------------------------------------------- /le-julia.el: -------------------------------------------------------------------------------- 1 | ;;; le-julia.el --- lispy support for Julia. -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2016-2019 Oleh Krehel 4 | 5 | ;; This file is not part of GNU Emacs 6 | 7 | ;; This file is free software; you can redistribute it and/or modify 8 | ;; it under the terms of the GNU General Public License as published by 9 | ;; the Free Software Foundation; either version 3, or (at your option) 10 | ;; any later version. 11 | 12 | ;; This program is distributed in the hope that it will be useful, 13 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ;; GNU General Public License for more details. 16 | 17 | ;; For a full copy of the GNU General Public License 18 | ;; see . 19 | 20 | ;;; Commentary: 21 | ;; 22 | 23 | ;;; Code: 24 | 25 | (require 'julia-mode nil t) 26 | (require 'julia-shell nil t) 27 | 28 | (declare-function julia-shell-collect-command-output "ext:julia-shell") 29 | 30 | (defun lispy--eval-julia (str) 31 | "Eval STR as Julia code." 32 | (string-trim-right (julia-shell-collect-command-output str))) 33 | 34 | ;; TODO: simplify 35 | (defun lispy-eval-julia-str (&optional bnd) 36 | (save-excursion 37 | (cond ((region-active-p) 38 | ;; get rid of "unexpected indent" 39 | (replace-regexp-in-string 40 | (concat 41 | "^" 42 | (save-excursion 43 | (goto-char (region-beginning)) 44 | (buffer-substring-no-properties 45 | (line-beginning-position) 46 | (point)))) 47 | "" (lispy--string-dwim))) 48 | ((looking-at lispy-outline) 49 | (string-trim-right 50 | (lispy--string-dwim 51 | (lispy--bounds-dwim)))) 52 | ((lispy-bolp) 53 | (lispy--string-dwim 54 | (lispy--bounds-c-toplevel))) 55 | (t 56 | (cond ((lispy-left-p)) 57 | ((lispy-right-p) 58 | (backward-list)) 59 | (t 60 | (error "Unexpected"))) 61 | (setq bnd (lispy--bounds-dwim)) 62 | (ignore-errors (backward-sexp)) 63 | (while (eq (char-before) ?.) 64 | (backward-sexp)) 65 | (setcar bnd (point)) 66 | (lispy--string-dwim bnd))))) 67 | 68 | (defun lispy-eval-julia () 69 | (lispy--eval-julia (lispy-eval-julia-str))) 70 | 71 | (provide 'le-julia) 72 | 73 | ;;; le-julia.el ends here 74 | -------------------------------------------------------------------------------- /le-scheme.el: -------------------------------------------------------------------------------- 1 | ;;; le-scheme.el --- lispy support for Scheme. -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2014-2015 Oleh Krehel 4 | 5 | ;; This file is not part of GNU Emacs 6 | 7 | ;; This file is free software; you can redistribute it and/or modify 8 | ;; it under the terms of the GNU General Public License as published by 9 | ;; the Free Software Foundation; either version 3, or (at your option) 10 | ;; any later version. 11 | 12 | ;; This program is distributed in the hope that it will be useful, 13 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ;; GNU General Public License for more details. 16 | 17 | ;; For a full copy of the GNU General Public License 18 | ;; see . 19 | 20 | ;;; Commentary: 21 | ;; 22 | 23 | ;;; Code: 24 | 25 | (eval-and-compile 26 | (require 'geiser-eval nil t)) 27 | 28 | (require 'lispy-inline) 29 | 30 | (defvar geiser-impl--implementation) 31 | (declare-function geiser-repl--connection* "geiser-repl") 32 | (declare-function run-geiser "geiser-repl") 33 | (declare-function geiser-repl--buffer-name "geiser-repl") 34 | (declare-function geiser-eval--send/wait "geiser-eval") 35 | (declare-function geiser-eval--retort-error "geiser-eval") 36 | (declare-function geiser-mode "geiser-mode") 37 | (declare-function geiser-edit-symbol "geiser-edit") 38 | (declare-function geiser-racket--language "geiser-racket") 39 | 40 | (defun lispy--eval-scheme (str) 41 | "Eval STR as Scheme code." 42 | (unless (geiser-repl--connection*) 43 | (save-window-excursion 44 | (if geiser-impl--implementation 45 | (run-geiser geiser-impl--implementation) 46 | (call-interactively 'run-geiser)) 47 | (geiser-mode 1))) 48 | (when (and (not (member (geiser-racket--language) '(plait))) 49 | (string-match "(\\(?:define\\|set!\\|struct\\)[ (]+\\(\\(?:\\w\\|\\s_\\)+\\)" str)) 50 | (let ((name (match-string 1 str))) 51 | (setq str (format "(begin %s %s)" str name)))) 52 | (with-current-buffer (geiser-repl--buffer-name geiser-impl--implementation) 53 | (let* ((code `(:eval (:scm ,str))) 54 | (ret (geiser-eval--send/wait code)) 55 | (err (geiser-eval--retort-error ret)) 56 | (output-str (cdr (assoc 'output ret))) 57 | (result-str (cadr (assoc 'result ret)))) 58 | (cond (err 59 | (format "Error: %s" (string-trim output-str))) 60 | ((not (equal "" output-str)) 61 | (concat 62 | (propertize 63 | output-str 64 | 'face 'font-lock-string-face) 65 | "\n" 66 | result-str)) 67 | (t 68 | result-str))))) 69 | 70 | (defun lispy-goto-symbol-scheme (symbol) 71 | (geiser-edit-symbol (make-symbol symbol))) 72 | 73 | (provide 'le-scheme) 74 | 75 | ;;; le-scheme.el ends here 76 | -------------------------------------------------------------------------------- /le-hy.el: -------------------------------------------------------------------------------- 1 | ;;; le-hy.el --- lispy support for Hy. -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2016 Oleh Krehel 4 | 5 | ;; This file is not part of GNU Emacs 6 | 7 | ;; This file is free software; you can redistribute it and/or modify 8 | ;; it under the terms of the GNU General Public License as published by 9 | ;; the Free Software Foundation; either version 3, or (at your option) 10 | ;; any later version. 11 | 12 | ;; This program is distributed in the hope that it will be useful, 13 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ;; GNU General Public License for more details. 16 | 17 | ;; For a full copy of the GNU General Public License 18 | ;; see . 19 | 20 | ;;; Commentary: 21 | ;; 22 | 23 | ;;; Code: 24 | 25 | (require 'hy-mode nil t) 26 | (require 'inf-lisp) 27 | 28 | (defun lispy--hy-proc () 29 | (let ((proc-name "hy")) 30 | (if (process-live-p proc-name) 31 | (get-process proc-name) 32 | (get-buffer-process 33 | (make-comint proc-name "hy"))))) 34 | 35 | (defun lispy--comint-eval (command) 36 | "Collect output of COMMAND without changing point." 37 | (let ((command-output-begin nil) 38 | (str nil) 39 | (last-cmd nil) 40 | (last-cmd-with-prompt nil) 41 | (buffer (process-buffer (lispy--hy-proc)))) 42 | (with-current-buffer buffer 43 | (let ((inhibit-field-text-motion t) 44 | (inhibit-read-only t)) 45 | ;; save the last command and delete the old prompt 46 | (beginning-of-line) 47 | (setq last-cmd-with-prompt 48 | (buffer-substring (point) (line-end-position))) 49 | (setq last-cmd (replace-regexp-in-string 50 | "=> " "" last-cmd-with-prompt)) 51 | (delete-region (point) (line-end-position)) 52 | ;; send the command 53 | (setq command-output-begin (point)) 54 | (comint-simple-send (get-buffer-process (current-buffer)) 55 | command) 56 | ;; collect the output 57 | (while (null (save-excursion 58 | (let ((inhibit-field-text-motion t)) 59 | (goto-char command-output-begin) 60 | (re-search-forward "^[. ]*=> \\s-*$" nil t)))) 61 | (accept-process-output (get-buffer-process buffer)) 62 | (goto-char (point-max))) 63 | (goto-char (point-max)) 64 | (when (looking-back "^[. ]*=> *" (line-beginning-position)) 65 | (goto-char (1- (match-beginning 0)))) 66 | ;; save output to string 67 | (setq str (buffer-substring-no-properties command-output-begin (point))) 68 | ;; delete the output from the command line 69 | (delete-region command-output-begin (point-max)) 70 | ;; restore prompt and insert last command 71 | (goto-char (point-max)) 72 | (comint-send-string (get-buffer-process (current-buffer)) "\n") 73 | (insert last-cmd) 74 | ;; return the shell output 75 | str)))) 76 | 77 | (defun lispy--eval-hy (str) 78 | "Eval STR as Hy code." 79 | (let ((res (lispy--comint-eval str))) 80 | (if (member res '("" "\n")) 81 | "(ok)" 82 | res))) 83 | 84 | (provide 'le-hy) 85 | 86 | ;;; le-hy.el ends here 87 | -------------------------------------------------------------------------------- /le-lisp.el: -------------------------------------------------------------------------------- 1 | ;;; le-lisp.el --- lispy support for Common Lisp. -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2014-2015 Oleh Krehel 4 | 5 | ;; This file is not part of GNU Emacs 6 | 7 | ;; This file is free software; you can redistribute it and/or modify 8 | ;; it under the terms of the GNU General Public License as published by 9 | ;; the Free Software Foundation; either version 3, or (at your option) 10 | ;; any later version. 11 | 12 | ;; This program is distributed in the hope that it will be useful, 13 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ;; GNU General Public License for more details. 16 | 17 | ;; For a full copy of the GNU General Public License 18 | ;; see . 19 | 20 | ;;; Commentary: 21 | ;; 22 | 23 | ;;; Code: 24 | (declare-function slime-output-buffer "ext:slime-repl") 25 | (declare-function slime "ext:slime") 26 | (declare-function slime-current-connection "ext:slime") 27 | (declare-function slime-eval "ext:slime") 28 | (declare-function slime-edit-definition "ext:slime") 29 | (declare-function sly-mrepl--find-buffer "ext:sly-mrepl") 30 | (declare-function sly "ext:sly") 31 | (declare-function sly-current-connection "ext:sly") 32 | (declare-function sly-eval "ext:sly") 33 | (declare-function sly-edit-definition "ext:sly") 34 | 35 | (defcustom lispy-use-sly nil 36 | "Whether to use SLY instead of SLIME." 37 | :group 'lispy 38 | :type 'boolean) 39 | 40 | (defun lispy--use-sly-p () 41 | (if lispy-use-sly 42 | (require 'sly) 43 | (unless (require 'slime nil t) 44 | (require 'sly) 45 | (setq lispy-use-sly t)))) 46 | 47 | (defun lispy--eval-lisp (str) 48 | "Eval STR as Common Lisp code." 49 | (let* ((deactivate-mark nil) 50 | (result (if (lispy--use-sly-p) 51 | (with-current-buffer (process-buffer (lispy--cl-process)) 52 | (sly-eval `(slynk:eval-and-grab-output ,str))) 53 | (slime-eval `(swank:eval-and-grab-output ,str))))) 54 | (pcase result 55 | (`("" "") "(ok)") 56 | (`("" ,val) val) 57 | (`(,out ,val) 58 | (concat (propertize (string-trim-left out) 'face 'font-lock-string-face) "\n\n" val))))) 59 | 60 | (defun lispy--cl-process () 61 | (unless (lispy--use-sly-p) 62 | (require 'slime-repl)) 63 | (or (if (lispy--use-sly-p) 64 | (sly-current-connection) 65 | (slime-current-connection)) 66 | (let (conn) 67 | (let ((wnd (current-window-configuration))) 68 | (if (lispy--use-sly-p) 69 | (sly) 70 | (slime)) 71 | (while (not (if (lispy--use-sly-p) 72 | (and (setq conn (sly-current-connection)) 73 | (sly-mrepl--find-buffer conn)) 74 | (and 75 | (setq conn (slime-current-connection)) 76 | (get-buffer-window (slime-output-buffer))))) 77 | (sit-for 0.2)) 78 | (set-window-configuration wnd) 79 | conn)))) 80 | 81 | (defun lispy--lisp-args (symbol) 82 | "Return a pretty string with arguments for SYMBOL." 83 | (let ((args 84 | (list 85 | (mapconcat 86 | #'prin1-to-string 87 | (read (lispy--eval-lisp 88 | (format (if (lispy--use-sly-p) 89 | "(slynk-backend:arglist #'%s)" 90 | "(swank-backend:arglist #'%s)") 91 | symbol))) 92 | " ")))) 93 | (if (listp args) 94 | (format 95 | "(%s %s)" 96 | (propertize symbol 'face 'lispy-face-hint) 97 | (mapconcat 98 | #'identity 99 | (mapcar (lambda (x) (propertize (downcase x) 100 | 'face 'lispy-face-req-nosel)) 101 | args) 102 | (concat "\n" 103 | (make-string (+ 2 (length symbol)) ?\ )))) 104 | (propertize args 'face 'lispy-face-hint)))) 105 | 106 | (defun lispy--lisp-describe (symbol) 107 | "Return documentation for SYMBOL." 108 | (read 109 | (lispy--eval-lisp 110 | (substring-no-properties 111 | (format 112 | "(let ((x '%s)) 113 | (or (if (boundp x) 114 | (documentation x 'variable) 115 | (documentation x 'function)) 116 | \"undocumented\"))" 117 | symbol))))) 118 | 119 | (defun lispy-flatten--lisp () 120 | (let* ((bnd (lispy--bounds-list)) 121 | (str (lispy--string-dwim bnd)) 122 | (expr (read str)) 123 | (fexpr (read (lispy--eval-lisp 124 | (format "(function-lambda-expression #'%S)" (car expr)))))) 125 | (if (not (eq (car-safe fexpr) 'SB-INT:NAMED-LAMBDA)) 126 | (error "Could not find the body of %S" (car expr)) 127 | (setq fexpr (downcase 128 | (prin1-to-string 129 | `(lambda ,(nth 2 fexpr) ,(cl-caddr (nth 3 fexpr)))))) 130 | (goto-char (car bnd)) 131 | (delete-region (car bnd) (cdr bnd)) 132 | (let* ((e-args (cdr expr)) 133 | (body (lispy--flatten-function fexpr e-args))) 134 | (lispy--insert body))))) 135 | 136 | (defun lispy-goto-symbol-lisp (symbol) 137 | ;; start SLY or SLIME if necessary 138 | (lispy--cl-process) 139 | (if (lispy--use-sly-p) 140 | (sly-edit-definition symbol) 141 | (slime-edit-definition symbol))) 142 | 143 | (provide 'le-lisp) 144 | 145 | ;;; le-lisp.el ends here 146 | -------------------------------------------------------------------------------- /doc/clojure.org: -------------------------------------------------------------------------------- 1 | * Middleware 2 | Lispy loads the middleware from =lispy-clojure.clj= that has the namespace =lispy-clojure=. 3 | 4 | ** Dependencies 5 | The Clojure middleware has only two base dependencies, and loads the others dynamically. 6 | 7 | If you use =lispy-eval= or =cider-jack-in= to connect, the dependencies are already added to 8 | =cider-jack-in-dependencies= automatically. 9 | 10 | Alternatively, add this to file:~/.lein/profiles.clj: 11 | #+begin_src clojure 12 | {:user {:dependencies 13 | [[org.tcrawley/dynapath "0.2.5"] 14 | [com.cemerick/pomegranate "0.4.0"]]}} 15 | #+end_src 16 | 17 | Alternatively, add this to file:/~/.clojure/deps.edn: 18 | #+begin_src clojure 19 | {:deps {org.tcrawley/dynapath {:mvn/version "0.2.5"} 20 | com.cemerick/pomegranate {:mvn/version "0.4.0"}}} 21 | #+end_src 22 | 23 | 24 | * Shadows 25 | Lispy tries to support a very REPL-driven approach to development. Both when writing new 26 | code and when debugging the old code, [[https://mitpress.mit.edu/sites/default/files/sicp/full-text/book/book-Z-H-10.html#%25_sec_1.1.5][Always Be Evaling]]. 27 | 28 | Unfortunately, things like let-bindings have an all-or-nothing eval: 29 | #+begin_src clojure 30 | (let [x1 (range 10) 31 | ;; => (0 1 2 3 4 5 6 7 8 9) 32 | x2 (map #(* % %) x1) 33 | x3 (partition 2 x2)] 34 | (second x3)) 35 | ;; => (4 9) 36 | #+end_src 37 | 38 | In the above example, we can eval only the first and the last step. But not the middle 39 | steps, since =x1= and =x2= are not bound. The middle steps are actually very important to 40 | understanding the context of the program. Imagine a sequence of functions calls from a 41 | library you're not particularly familiar with: seeing their return result is a good way to 42 | familiarize yourself with them. 43 | 44 | Here's a simplistic way to overcome it: 45 | #+begin_src clojure 46 | (do (def x1 (range 10)) x1) 47 | ;; => (0 1 2 3 4 5 6 7 8 9) 48 | (do (def x2 (map #(* % %) x1)) x2) 49 | ;; => (0 1 4 9 16 25 36 49 64 81) 50 | (do (def x3 (partition 2 x2)) x3) 51 | ;; => ((0 1) (4 9) (16 25) (36 49) (64 81)) 52 | (second x3) 53 | ;; => (4 9) 54 | #+end_src 55 | 56 | Two problems with this approach: 57 | - We have to rewrite the code 58 | - We pollute the namespace 59 | 60 | Lispy solves both of these problems. Firstly, an equivalent of the above code is run 61 | without having to rewrite it. Simply press ~e~ on the value of the binding. 62 | 63 | Secondly, the namespace isn't actully polluted. These "shadow" bindings are stored into a 64 | single dictionary variable in the current namespace called =shadows=. And when you call ~e~ on 65 | the third expression =(partition 2 x2)=, this is what =lispy.el= passes to the middleware: 66 | #+begin_src clojure 67 | (lispy.clojure/reval 68 | "(partition 2 x2)" 69 | "[x1 (range 10)\n x2 (map #(* %) x1)\n x3 (partition 2 x2)]" 70 | "test.clj" 1) 71 | #+end_src 72 | 73 | which expands to (I used ~xj~ and ~2e~ to eval and insert all those comments in place): 74 | #+begin_src clojure 75 | (ns lispy.clojure) 76 | 77 | (let [x1 (range 10) 78 | x2 (map #(* % %) x1) 79 | x3 (partition 2 x2)] 80 | (second x3)) 81 | 82 | (defn reval [e-str context-str & {:keys [file line]}] 83 | (let [expr (read-string e-str) 84 | ;; => (partition 2 x2) 85 | context (try 86 | (read-string context-str) 87 | (catch Exception _)) 88 | ;; => 89 | ;; [x1 90 | ;; (range 10) 91 | ;; x2 92 | ;; (map (fn* [p1__8686#] (* p1__8686#)) x1) 93 | ;; x3 94 | ;; (partition 2 x2)] 95 | full-expr (read-string (format "[%s]" e-str)) 96 | ;; => [(partition 2 x2)] 97 | expr1 (xcond 98 | ((nil? context-str) 99 | ;; => false 100 | (cons 'do full-expr)) 101 | ((= (count full-expr) 2) 102 | ;; => false 103 | (shadow-dest full-expr)) 104 | ((add-location-to-deflike expr file line) 105 | ;; => nil 106 | ) 107 | (:else 108 | (guess-intent expr context) 109 | ;; => (partition 2 x2) 110 | )) 111 | ;; => 112 | ;; (clojure.core/let 113 | ;; [x3 (partition 2 x2)] 114 | ;; (lispy.clojure/shadow-def 'x3 x3) 115 | ;; {:x3 x3}) 116 | ] 117 | (eval `(with-shadows 118 | (try 119 | (do ~expr1) 120 | (catch Exception ~'e 121 | (clojure.core/str "error: " ~ 'e)))) 122 | ;; => 123 | ;; (lispy.clojure/with-shadows 124 | ;; (try 125 | ;; (do 126 | ;; (clojure.core/let 127 | ;; [x3 (partition 2 x2)] 128 | ;; (lispy.clojure/shadow-def 'x3 x3) 129 | ;; {:x3 x3})) 130 | ;; (catch java.lang.Exception e (clojure.core/str "error: " e)))) 131 | ) 132 | ;; => {:x3 ((0 1) (4 9) (16 25) (36 49) (64 81))} 133 | )) 134 | #+end_src 135 | 136 | And finally, the expansion =lispy.clojure/with-shadows= is passed to =eval=: 137 | #+begin_src clojure 138 | (let* [x1 ((lispy.clojure/shadow-map) "x1") 139 | x2 ((lispy.clojure/shadow-map) "x2")] 140 | (try 141 | (do 142 | (clojure.core/let 143 | [x3 (partition 2 x2)] 144 | (lispy.clojure/shadow-def 145 | (quote x3) 146 | x3) 147 | {:x3 x3})) 148 | (catch java.lang.Exception 149 | e 150 | (clojure.core/str "error: " e)))) 151 | ;; => {:x3 ((0 1) (4 9) (16 25) (36 49) (64 81))} 152 | #+end_src 153 | 154 | Here's a quick way to clean up all shadow variables: 155 | #+begin_src clojure 156 | (lispy.clojure/shadow-unmap *ns*) 157 | #+end_src 158 | -------------------------------------------------------------------------------- /lispy-clojure-test.clj: -------------------------------------------------------------------------------- 1 | ;;; lispy-clojure-test.clj --- lispy support for Clojure. 2 | 3 | ;; Copyright (C) 2018 Oleh Krehel 4 | 5 | ;; This file is not part of GNU Emacs 6 | 7 | ;; This file is free software; you can redistribute it and/or modify 8 | ;; it under the terms of the GNU General Public License as published by 9 | ;; the Free Software Foundation; either version 3, or (at your option) 10 | ;; any later version. 11 | 12 | ;; This program is distributed in the hope that it will be useful, 13 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ;; GNU General Public License for more details. 16 | 17 | ;; For a full copy of the GNU General Public License 18 | ;; see . 19 | 20 | (ns lispy-clojure-test 21 | (:require 22 | [clojure.test :refer [is deftest]] 23 | [lispy.clojure :refer [add-location-to-defn 24 | add-location-to-def 25 | debug-step-in 26 | dest 27 | get-func-args 28 | get-func-args-def 29 | guess-intent 30 | object-members 31 | position 32 | reader= 33 | reval 34 | symbol-function]])) 35 | 36 | (deftest get-func-args-test 37 | (is (= (get-func-args (symbol-function 'string?) 1) '[x])) 38 | (is (= (get-func-args (symbol-function 'to-array) 1) '[coll]))) 39 | 40 | (deftest get-func-args-def-test 41 | (is (= (get-func-args-def (symbol-function 'defn) 2) 42 | '[name & fdecl]))) 43 | 44 | (deftest object-members-test 45 | (is (= ((into #{} (object-members Thread)) "run") "run"))) 46 | 47 | (deftest dest-test 48 | (is (= (eval (dest '[[x y] (list 1 2 3)])) 49 | {:x 1, :y 2})) 50 | (is (= (eval (dest '[[x & y] [1 2 3]])) 51 | {:x 1, :y '(2 3)})) 52 | (is (= (eval (dest '[[x y] (list 1 2 3) [a b] [y x]])) 53 | {:x 1, :y 2, :a 2, :b 1})) 54 | (is (= (eval (dest '[[x y z] [1 2]])) 55 | {:x 1, :y 2, :z nil})) 56 | (is (= (eval (dest '[[x & tail :as all] [1 2 3]])) 57 | {:x 1, 58 | :tail '(2 3), 59 | :all [1 2 3]})) 60 | (is (= (eval (dest '[[x & tail :as all] "Clojure"])) 61 | {:x \C, 62 | :tail '(\l \o \j \u \r \e), 63 | :all "Clojure"})) 64 | (is (= (eval (dest '[{x 1 y 2} {1 "one" 2 "two" 3 "three"}])) 65 | {:x "one", :y "two"})) 66 | (is (= (eval (dest '[{x 1 y 2 :or {x "one" y "two"} :as all} {2 "three"}])) 67 | {:all {2 "three"}, 68 | :x "one", 69 | :y "three"})) 70 | (is (= (eval (dest '[{:keys [x y]} {:x "one" :z "two"}])) 71 | {:x "one", :y nil})) 72 | (is (= (eval (dest '[{:strs [x y]} {"x" "one" "z" "two"}])) 73 | {:x "one", :y nil})) 74 | (is (= (eval (dest '[{:syms [x y]} {'x "one" 'z "two"}])) 75 | {:x "one", :y nil}))) 76 | 77 | (deftest debug-step-in-test 78 | (is (= (debug-step-in 79 | '(lispy.clojure/expand-home (str "/foo" "/bar"))) 80 | {:path "/foo/bar"})) 81 | (is 82 | (= 83 | ((juxt :file :line) 84 | (debug-step-in 85 | '(lispy.clojure/add-location-to-def '(def x 1) "/foo/bar.clj" 42))) 86 | ["/foo/bar.clj" 42]))) 87 | 88 | (deftest reader=-test 89 | (is (reader= '(map #(* % %) '(1 2 3)) 90 | '(map #(* % %) '(1 2 3)))) 91 | (is (reader= #"regex" #"regex")) 92 | (is (not (= #"regex" #"regex")))) 93 | 94 | (deftest position-test 95 | (let [x (read-string "(map #(* % %) as)") 96 | c (read-string "[as (range 10) bs (map #(* % %) as)]")] 97 | (is (= (position x c =) nil)) 98 | (is (= (position x c reader=) 3)))) 99 | 100 | (deftest add-location-to-defn-test 101 | (is (= (add-location-to-defn 102 | '(defn fun 103 | "doc" 104 | ([x1 x2]) 105 | ([x1])) 106 | "/foo/bar.clj" 42) 107 | '(defn fun 108 | "doc" 109 | {:l-file "/foo/bar.clj", 110 | :l-line 42} 111 | ([x1 x2]) 112 | ([x1])))) 113 | (is (= (add-location-to-defn 114 | '(defn fun 115 | "doc" 116 | [x] 117 | x) 118 | "/foo/bar.clj" 42) 119 | '(defn fun 120 | "doc" 121 | {:l-file "/foo/bar.clj", 122 | :l-line 42} 123 | [x] 124 | x))) 125 | (is (= (add-location-to-defn 126 | '(defn fun [x] 127 | x) 128 | "/foo/bar.clj" 42) 129 | '(defn fun 130 | "" 131 | {:l-file "/foo/bar.clj", 132 | :l-line 42} 133 | [x] 134 | x)))) 135 | 136 | (deftest add-location-to-def-test 137 | (let [e (add-location-to-def 138 | '(def asdf 1) "/foo/bar.clj" 42) 139 | e2 (add-location-to-def 140 | '(def asdf "doc" 1) "/foo/bar.clj" 42)] 141 | (is (= e '(def asdf "" 1))) 142 | (is (= e2 '(def asdf "doc" 1))) 143 | (is (= ((juxt :l-file :l-line) (meta (eval e))) 144 | ((juxt :l-file :l-line) (meta (eval e2))) 145 | ["/foo/bar.clj" 42])))) 146 | 147 | (deftest guess-intent-test 148 | (is (= (guess-intent 'x '[x y]) 'x)) 149 | (is (= (guess-intent '*ns* '*ns*) '*ns*)) 150 | (is (= (guess-intent '(+ 1 2) '[(+ 1 2) (+ 3 4) (+ 5 6)]) 151 | '(+ 1 2)))) 152 | 153 | (deftest reval-test 154 | (let [s "(->> 5 155 | (range) 156 | (map (fn [x] (* x x))) 157 | (map (fn [x] (+ x x))))"] 158 | (is (= (reval "(range)" s) 159 | '(0 1 2 3 4))) 160 | (is (= (reval "(map (fn [x] (* x x)))" s) 161 | '(0 1 4 9 16))) 162 | (is (= (reval "(map (fn [x] (+ x x)))" s) 163 | '(0 2 8 18 32)))) 164 | (is (= (reval "x (+ 2 2)" "[x (+ 2 2)]") {:x 4})) 165 | (is (= (reval "(+ 2 2)" "[x (+ 2 2)]") {:x 4})) 166 | (is (= (reval "(+ 2 2)" "[x (+ 1 2) y (+ 2 2)]") {:y 4})) 167 | (is (= (reval "(range 5)" "[[x & y] (range 5)]") 168 | {:x 0, :y '(1 2 3 4)})) 169 | (is (= (reval "[c [(* 2 21) 0]]" "(doseq [c [(* 2 21) 0]]\n ())") {:c 42})) 170 | (is (= (reval "[i 3]" "(dotimes [i 3])") {:i 0})) 171 | (is (= (reval "[a b] (range 5)" "[[a b] (range 5)]") {:a 0, :b 1})) 172 | (let [js "(doto (new java.util.HashMap) (.put \"a\" 1) (.put \"b\" 2))"] 173 | (is (= (reval "(.put \"a\" 1)" js) {"a" 1})) 174 | (is (= (reval "(.put \"b\" 2)" js) {"a" 1, "b" 2}))) 175 | (is (= (reval "(def x1 1)\n(+ x1 x1)" nil) 2))) 176 | 177 | (deftest format-ctor-test 178 | (is (= (lispy.clojure/format-ctor "protected java.awt.Graphics2D()") "java.awt.Graphics2D."))) 179 | 180 | (deftest read-string-all-test 181 | (is (= (lispy.clojure/read-string-all "(foo) (bar) \"baz\"") 182 | '[(foo) (bar) "baz"]))) 183 | 184 | (clojure.test/run-tests 'lispy-clojure-test) 185 | -------------------------------------------------------------------------------- /test/test_lispy-python.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import re 4 | import ast 5 | import sys 6 | from importlib.machinery import SourceFileLoader 7 | from contextlib import redirect_stdout 8 | from textwrap import dedent 9 | 10 | lp = SourceFileLoader("lispy-python", os.path.dirname(__file__) + "/../lispy-python.py").load_module() 11 | 12 | def print_elisp_to_str(obj): 13 | with io.StringIO() as buf, redirect_stdout(buf): 14 | lp.print_elisp(obj) 15 | return buf.getvalue().strip() 16 | 17 | 18 | def with_output_to_string(code): 19 | with io.StringIO() as buf, redirect_stdout(buf): 20 | exec(code) 21 | return buf.getvalue().strip() 22 | 23 | def lp_eval(code, env={}): 24 | return with_output_to_string(f"__res__=lp.eval_code('''{code}''', {env})") 25 | 26 | def test_exec(): 27 | # Need to run this with globals(). 28 | # Otherwise, x will not be defined later 29 | exec("x=1", globals()) 30 | assert x == 1 31 | 32 | def test_tr_returns_1(): 33 | code_1 = dedent(""" 34 | x=1 35 | y=2 36 | return x + y""") 37 | parsed = ast.parse(code_1).body 38 | assert len(parsed) == 3 39 | translated = lp.tr_returns(parsed) 40 | assert len(translated) == 3 41 | assert parsed[0] == translated[0] 42 | assert parsed[1] == translated[1] 43 | assert ast.unparse(translated[2]) == "return locals() | {'__return__': x + y}" 44 | code = ast.unparse(lp.wrap_return(translated)) 45 | exec(code, globals()) 46 | assert x == 1 47 | assert y == 2 48 | assert __return__ == 3 49 | 50 | def test_tr_returns_2(): 51 | code_2 = dedent(""" 52 | if os.environ.get("FOO"): 53 | return 0 54 | x = 1 55 | y = 2 56 | return x + y""") 57 | parsed = ast.parse(code_2).body 58 | assert len(parsed) == 4 59 | translated = lp.tr_returns(parsed) 60 | assert len(translated) == 4 61 | assert ast.unparse(translated[3]) == "return locals() | {'__return__': x + y}" 62 | assert ast.unparse(translated[0].body) == "return locals() | {'__return__': 0}" 63 | code = ast.unparse(lp.wrap_return(translated)) 64 | exec(code, globals()) 65 | assert x == 1 66 | assert y == 2 67 | assert __return__ == 3 68 | os.environ["FOO"] = "BAR" 69 | exec(code, globals()) 70 | assert __return__ == 0 71 | 72 | def test_translate_def(): 73 | code = dedent(""" 74 | def add(x, y): 75 | return x + y 76 | """) 77 | tr = lp.translate(code) 78 | assert len(tr) == 1 79 | assert isinstance(tr[0], ast.FunctionDef) 80 | r = lp.eval_code(code) 81 | assert r["res"] == "'unset'" 82 | assert "" \\)', r) 227 | -------------------------------------------------------------------------------- /lispy-occur.el: -------------------------------------------------------------------------------- 1 | ;;; lispy-occur.el --- Select a line within the current top level sexp. -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2014-2021 Oleh Krehel 4 | 5 | ;; This file is not part of GNU Emacs 6 | 7 | ;; This file is free software; you can redistribute it and/or modify 8 | ;; it under the terms of the GNU General Public License as published by 9 | ;; the Free Software Foundation; either version 3, or (at your option) 10 | ;; any later version. 11 | 12 | ;; This program is distributed in the hope that it will be useful, 13 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ;; GNU General Public License for more details. 16 | 17 | ;; For a full copy of the GNU General Public License 18 | ;; see . 19 | 20 | ;;; Commentary: 21 | ;; 22 | 23 | ;;; Code: 24 | (require 'swiper) 25 | 26 | (defcustom lispy-occur-backend 'ivy 27 | "Method to navigate to a line with `lispy-occur'." 28 | :type '(choice 29 | (const :tag "Ivy" ivy) 30 | (const :tag "Helm" helm))) 31 | 32 | (defvar lispy--occur-beg 1 33 | "Start position of the top level sexp during `lispy-occur'.") 34 | 35 | (defvar lispy--occur-end 1 36 | "End position of the top level sexp during `lispy-occur'.") 37 | 38 | (defun lispy--occur-candidates (&optional bnd) 39 | "Return the candidates for `lispy-occur'." 40 | (setq bnd (or bnd (save-excursion 41 | (unless (and (bolp) 42 | (lispy-left-p)) 43 | (beginning-of-defun)) 44 | (lispy--bounds-dwim)))) 45 | (let ((line-number -1) 46 | candidates) 47 | (setq lispy--occur-beg (car bnd)) 48 | (setq lispy--occur-end (cdr bnd)) 49 | (save-excursion 50 | (goto-char lispy--occur-beg) 51 | (while (< (point) lispy--occur-end) 52 | (push (format "%-3d %s" 53 | (cl-incf line-number) 54 | (buffer-substring 55 | (line-beginning-position) 56 | (line-end-position))) 57 | candidates) 58 | (forward-line 1))) 59 | (nreverse candidates))) 60 | 61 | (defun lispy--occur-preselect () 62 | "Initial candidate regex for `lispy-occur'." 63 | (format "^%d" 64 | (- 65 | (line-number-at-pos (point)) 66 | (line-number-at-pos lispy--occur-beg)))) 67 | 68 | (defvar helm-input) 69 | (declare-function helm "ext:helm") 70 | 71 | (defun lispy-occur-action-goto-paren (x) 72 | "Goto line X for `lispy-occur'." 73 | (setq x (read x)) 74 | (goto-char lispy--occur-beg) 75 | (let ((input (if (eq lispy-occur-backend 'helm) 76 | helm-input 77 | ivy-text)) 78 | str-or-comment) 79 | (cond ((string= input "") 80 | (forward-line x) 81 | (back-to-indentation) 82 | (when (re-search-forward lispy-left (line-end-position) t) 83 | (goto-char (match-beginning 0)))) 84 | 85 | ((setq str-or-comment 86 | (progn 87 | (forward-line x) 88 | (re-search-forward (ivy--regex input) 89 | (line-end-position) t) 90 | (lispy--in-string-or-comment-p))) 91 | (goto-char str-or-comment)) 92 | 93 | ((re-search-backward lispy-left (line-beginning-position) t) 94 | (goto-char (match-beginning 0))) 95 | 96 | ((re-search-forward lispy-left (line-end-position) t) 97 | (goto-char (match-beginning 0))) 98 | 99 | (t 100 | (back-to-indentation))))) 101 | 102 | (defun lispy-occur-action-goto-end (x) 103 | "Goto line X for `lispy-occur'." 104 | (setq x (read x)) 105 | (goto-char lispy--occur-beg) 106 | (forward-line x) 107 | (re-search-forward (ivy--regex ivy-text) (line-end-position) t)) 108 | 109 | (defun lispy-occur-action-goto-beg (x) 110 | "Goto line X for `lispy-occur'." 111 | (when (lispy-occur-action-goto-end x) 112 | (goto-char (match-beginning 0)))) 113 | 114 | (defun lispy-occur-action-mc (_x) 115 | "Make a fake cursor for each `lispy-occur' candidate." 116 | (let ((cands (nreverse ivy--old-cands)) 117 | cand) 118 | (while (setq cand (pop cands)) 119 | (goto-char lispy--occur-beg) 120 | (forward-line (read cand)) 121 | (re-search-forward (ivy--regex ivy-text) (line-end-position) t) 122 | (when cands 123 | (mc/create-fake-cursor-at-point)))) 124 | (multiple-cursors-mode 1)) 125 | 126 | (ivy-set-actions 127 | 'lispy-occur 128 | '(("m" lispy-occur-action-mc "multiple-cursors") 129 | ("j" lispy-occur-action-goto-beg "goto start") 130 | ("k" lispy-occur-action-goto-end "goto end"))) 131 | 132 | (defvar ivy-last) 133 | (declare-function ivy-state-window "ext:ivy") 134 | 135 | ;;;###autoload 136 | (defun lispy-occur () 137 | "Select a line within current top level sexp. 138 | See `lispy-occur-backend' for the selection back end." 139 | (interactive) 140 | (swiper--init) 141 | (cond ((eq lispy-occur-backend 'helm) 142 | (require 'helm) 143 | (add-hook 'helm-move-selection-after-hook 144 | #'lispy--occur-update-input-helm) 145 | (add-hook 'helm-update-hook 146 | #'lispy--occur-update-input-helm) 147 | (unwind-protect 148 | (helm :sources 149 | `((name . "this defun") 150 | (candidates . ,(lispy--occur-candidates)) 151 | (action . lispy-occur-action-goto-paren) 152 | (match-strict . 153 | (lambda (x) 154 | (ignore-errors 155 | (string-match 156 | (ivy--regex helm-input) x))))) 157 | :preselect (lispy--occur-preselect) 158 | :buffer "*lispy-occur*") 159 | (swiper--cleanup) 160 | (remove-hook 'helm-move-selection-after-hook 161 | #'lispy--occur-update-input-helm) 162 | (remove-hook 'helm-update-hook 163 | #'lispy--occur-update-input-helm))) 164 | ((eq lispy-occur-backend 'ivy) 165 | (unwind-protect 166 | (ivy-read "pattern: " 167 | (lispy--occur-candidates) 168 | :preselect (lispy--occur-preselect) 169 | :require-match t 170 | :update-fn (lambda () 171 | (lispy--occur-update-input 172 | ivy-text 173 | (ivy-state-current ivy-last))) 174 | :action #'lispy-occur-action-goto-paren 175 | :caller 'lispy-occur) 176 | (swiper--cleanup) 177 | (when (null ivy-exit) 178 | (goto-char swiper--opoint)))) 179 | (t 180 | (error "Bad `lispy-occur-backend': %S" lispy-occur-backend)))) 181 | 182 | (defun lispy--occur-update-input-helm () 183 | "Update selection for `lispy-occur' using `helm' back end." 184 | (lispy--occur-update-input 185 | helm-input 186 | (buffer-substring-no-properties 187 | (line-beginning-position) 188 | (line-end-position)))) 189 | 190 | (defun lispy--occur-update-input (input str) 191 | "Update selection for `ivy-occur'. 192 | INPUT is the current input text. 193 | STR is the full current candidate." 194 | (swiper--cleanup) 195 | (let ((re (ivy--regex input)) 196 | (num (if (string-match "^[0-9]+" str) 197 | (string-to-number (match-string 0 str)) 198 | 0))) 199 | (with-selected-window (ivy-state-window ivy-last) 200 | (goto-char lispy--occur-beg) 201 | (when (cl-plusp num) 202 | (forward-line num) 203 | (unless (<= (point) lispy--occur-end) 204 | (recenter))) 205 | (let ((ov (make-overlay (line-beginning-position) 206 | (1+ (line-end-position))))) 207 | (overlay-put ov 'face 'swiper-line-face) 208 | (overlay-put ov 'window (ivy-state-window ivy-last)) 209 | (push ov swiper--overlays)) 210 | (re-search-forward re (line-end-position) t) 211 | (swiper--add-overlays 212 | re 213 | lispy--occur-beg 214 | lispy--occur-end)))) 215 | 216 | ;;; lispy-occur.el ends here 217 | -------------------------------------------------------------------------------- /lispy-tags.el: -------------------------------------------------------------------------------- 1 | ;;; lispy-tags.el --- Facilitate getting a pretty list of tags for many files -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2015 Oleh Krehel 4 | 5 | ;; This file is not part of GNU Emacs 6 | 7 | ;; This file is free software; you can redistribute it and/or modify 8 | ;; it under the terms of the GNU General Public License as published by 9 | ;; the Free Software Foundation; either version 3, or (at your option) 10 | ;; any later version. 11 | 12 | ;; This program is distributed in the hope that it will be useful, 13 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ;; GNU General Public License for more details. 16 | 17 | ;; For a full copy of the GNU General Public License 18 | ;; see . 19 | 20 | ;;; Commentary: 21 | ;; 22 | 23 | ;;; Code: 24 | 25 | (require 'cl-lib) 26 | 27 | (defvar lispy-tag-arity) ;; defined in lispy 28 | 29 | (declare-function semantic-new-buffer-fcn "semantic") 30 | (declare-function semantic-parse-region "semantic") 31 | (declare-function semantic-mode "semantic") 32 | (declare-function semantic-parse-tree-state "semantic") 33 | (declare-function semantic-tag-overlay "tag") 34 | (declare-function semantic-tag-get-attribute "tag") 35 | (declare-function semantic-tag-name "tag") 36 | (declare-function semantic-current-tag "find") 37 | (declare-function lispy--tag-regexp "lispy") 38 | (declare-function lispy--modify-tag "lispy") 39 | (declare-function lispy--tag-name "lispy") 40 | 41 | (defvar lispy-db (make-hash-table :test 'equal) 42 | "An alist of file to a pretty list of tags.") 43 | 44 | (cl-defstruct lispy-dbfile 45 | file 46 | tags 47 | modtime 48 | plain-tags) 49 | 50 | (defun lispy--file-list () 51 | "Get the list of same type files in current directory." 52 | (and (buffer-file-name) 53 | (let ((ext (file-name-extension (buffer-file-name)))) 54 | (nreverse 55 | (cl-remove-if 56 | (lambda (x) (string-match "\\(?:^\\.?#\\|~$\\|loaddefs.el\\)" x)) 57 | (file-expand-wildcards (format "*.%s" ext))))))) 58 | 59 | (define-error 'no-semantic-support "No semantic support for major-mode") 60 | 61 | (defun lispy--fetch-this-file-tags (&optional file) 62 | "Fetch tags for FILE." 63 | (setq file (or file (buffer-file-name))) 64 | (semantic-new-buffer-fcn) 65 | (when (null semantic--parse-table) 66 | (signal 'no-semantic-support nil)) 67 | (let ((tags (semantic-parse-region (point-min) (point-max)))) 68 | (when (memq major-mode (cons 'lisp-mode lispy-elisp-modes)) 69 | (let ((arity (cdr (assoc major-mode lispy-tag-arity))) 70 | (tag-regex (lispy--tag-regexp))) 71 | (mapc (lambda (x) (lispy--modify-tag x tag-regex arity file)) tags))) 72 | tags)) 73 | 74 | (defun lispy-build-semanticdb (&optional dir) 75 | "Build and save semanticdb for DIR." 76 | (interactive) 77 | (setq dir (or dir default-directory)) 78 | (let ((default-directory dir)) 79 | (dolist (f (lispy--file-list)) 80 | (let ((buf (get-file-buffer f))) 81 | (with-current-buffer (find-file-noselect f) 82 | (semantic-mode 1) 83 | (let ((semantic-parse-tree-state 'needs-rebuild)) 84 | (lispy--fetch-this-file-tags)) 85 | (unless buf 86 | (kill-buffer)))))) 87 | (let ((db (semanticdb-directory-loaded-p dir))) 88 | (or (semanticdb-save-db db) db))) 89 | 90 | (defvar lispy-completion-method) 91 | (defvar lispy-helm-columns) 92 | 93 | (defun lispy--format-tag-line (x) 94 | "Add file name to (`lispy--tag-name' X)." 95 | (if (and (eq lispy-completion-method 'ido) 96 | (not (or (bound-and-true-p ido-vertical-mode) 97 | (bound-and-true-p ivy-mode)))) 98 | x 99 | (let* ((width (min (- (window-width) 100 | (if (and (boundp 'fringe-mode) 101 | (not (eq fringe-mode 0))) 0 1)) 102 | (cadr lispy-helm-columns))) 103 | (s1 (car x)) 104 | (s2 (file-name-nondirectory 105 | (cadr x)))) 106 | (cons (if (< width 50) 107 | (if (> (length s1) width) 108 | (concat (substring s1 0 (- width 3)) 109 | "...") 110 | s1) 111 | (format (format "%%s%% %ds" (- width 112 | (length s1))) 113 | s1 s2)) 114 | x)))) 115 | 116 | (defun lispy--file-fresh-p (actual-time stored-time) 117 | "Return t when ACTUAL-TIME isn't much larger than STORED-TIME." 118 | (and stored-time 119 | (< (time-to-seconds 120 | (time-subtract 121 | actual-time 122 | stored-time)) 123 | 1.0))) 124 | 125 | (defvar lispy-force-reparse nil 126 | "When non-nil, ignore that tags are up-to-date and parse anyway.") 127 | 128 | (defmacro lispy-with-mode-off (mode &rest body) 129 | "Run BODY with MODE off and re-enable it if it was on." 130 | (declare (indent 1) 131 | (debug (form body))) 132 | `(let ((mode-was-on (bound-and-true-p ,mode)) 133 | res) 134 | (if mode-was-on 135 | (progn 136 | (let ((inhibit-message t)) 137 | (,mode -1)) 138 | (unwind-protect 139 | (setq res (progn ,@body)) 140 | (let ((inhibit-message t)) 141 | (,mode 1))) 142 | res) 143 | ,@body))) 144 | 145 | (defun lispy--fetch-tags (&optional file-list) 146 | "Get a list of tags for FILE-LIST." 147 | (require 'semantic/bovine/el) 148 | (lispy-with-mode-off recentf-mode 149 | (setq file-list (or file-list (lispy--file-list))) 150 | (let (res dbfile db-to-save) 151 | (dolist (file file-list) 152 | (let ((file-modtime (nth 5 (file-attributes file 'integer))) 153 | (exfile (expand-file-name file))) 154 | (unless (and (null lispy-force-reparse) 155 | (setq dbfile 156 | (gethash exfile lispy-db)) 157 | (lispy--file-fresh-p 158 | file-modtime 159 | (lispy-dbfile-modtime dbfile)) 160 | (lispy-dbfile-tags dbfile)) 161 | (let ((table (semanticdb-create-table-for-file (expand-file-name file)))) 162 | (if (null table) 163 | (error "Couldn't open semanticdb for file: %S" file) 164 | (let ((db (car table)) 165 | (table (cdr table)) 166 | tags) 167 | (unless (and (null lispy-force-reparse) 168 | (lispy--file-fresh-p 169 | file-modtime 170 | (oref table lastmodtime)) 171 | (setq tags 172 | (ignore-errors 173 | (oref table tags))) 174 | (semantic-tag-overlay (car-safe tags)) 175 | (not (eq (cadr (car-safe tags)) 'code))) 176 | (let ((buf (get-file-buffer file))) 177 | (with-current-buffer (or buf (find-file-noselect file)) 178 | (semantic-new-buffer-fcn) 179 | (semantic-mode 1) 180 | (oset table tags 181 | (let ((semantic-parse-tree-state 'needs-update)) 182 | (lispy--fetch-this-file-tags file))) 183 | (oset table lastmodtime 184 | (current-time)) 185 | (semanticdb-set-dirty table) 186 | (cl-pushnew db db-to-save) 187 | (unless buf 188 | (kill-buffer))))) 189 | (puthash 190 | exfile 191 | (setq dbfile 192 | (make-lispy-dbfile 193 | :file file 194 | :modtime (oref table lastmodtime) 195 | :tags (mapcar 196 | (lambda (x) 197 | (lispy--make-tag x exfile)) 198 | (oref table tags)) 199 | :plain-tags (oref table tags))) 200 | lispy-db))))) 201 | (setq res (append (lispy-dbfile-tags dbfile) res)))) 202 | (dolist (db db-to-save) 203 | (semanticdb-save-db db)) 204 | res))) 205 | 206 | (defun lispy--make-tag (tag file) 207 | "Construct a modified TAG entry including FILE." 208 | (list (lispy--tag-name tag file) 209 | file 210 | (semantic-tag-overlay tag))) 211 | 212 | 213 | (provide 'lispy-tags) 214 | 215 | ;;; lispy-tags.el ends here 216 | -------------------------------------------------------------------------------- /lispytutor/lispytutor.el: -------------------------------------------------------------------------------- 1 | ;;* Welcome to the `lispy-mode' Tutor 2 | ;; 3 | ;; `lispy-mode' is a very powerful way to edit LISP code that has many 4 | ;; commands, too many to explain in a tutor such as this. This tutor 5 | ;; is designed to describe enough of the commands that you will be 6 | ;; able to easily use `lispy-mode' as an all-purpose LISP editor. 7 | ;; 8 | ;; The approximate time required to complete the tutor is 25-30 9 | ;; minutes, depending upon how much time is spent with 10 | ;; experimentation. 11 | ;; 12 | ;; ATTENTION: 13 | ;; The commands in the lessons will modify the code. You can always 14 | ;; revert the file using git. 15 | ;; 16 | ;; It is important to remember that this tutor is set up to teach by 17 | ;; use. That means that you need to execute the commands to learn 18 | ;; them properly. If you only read the text, you will forget the 19 | ;; commands! 20 | ;; 21 | ;; Remember that this is Emacs, you can rebind any binding that you 22 | ;; don't like. However, a lot of thought has been put into the current 23 | ;; bindings so don't be surprised if you find stuff inconvenient after 24 | ;; rebinding without thinking it through. 25 | ;; 26 | ;; Your point should now be at beginning of the first line. 27 | ;; Press =M-<= if it isn't. 28 | ;; 29 | ;; Press =J= to move to the next outline downwards. 30 | ;; 31 | ;; You should now be at Lesson 1.1. 32 | ;; 33 | ;; Press =i= to show the lesson. 34 | ;; Press =N= to narrow to the lesson. 35 | ;; 36 | ;;* Lesson 1.1: TUTORIAL NAVIGATION 37 | ;; This tutorial uses lispy outlines for navigation. 38 | ;; 39 | ;; Outlines provide a way of navigating code in a tree-like fashion. 40 | ;; The outlines themselves form the nodes of the tree. 41 | ;; 42 | ;; An outline begins with a comment ";;" followed by one or more 43 | ;; asterisks. The number of asterisks determines the outline's depth 44 | ;; in the tree. Your cursor should be at the start of the outline: 45 | ;; ";;* Lesson 1.1: TUTORIAL NAVIGATION" 46 | ;; 47 | ;; Place it there if it isn't already. 48 | ;; 49 | ;; Lispy enables special keybindings when your cursor is placed on an 50 | ;; object lispy recognizes. 51 | ;; 52 | ;; Here's a few to get started: 53 | ;; =J= moves to the next outline 54 | ;; =K= moves to the previous outline 55 | ;; =i= cycles the expansion of the outline and its children. 56 | ;; 57 | ;; Press =J= and =K= a few times to navigate the outlines in this lesson. 58 | ;; Try cycling the expansion of outlines with =i=. 59 | ;; 60 | ;; When you're finished proceed to Exercise 1. 61 | ;; 62 | ;;** Exercise 1 63 | ;; Lispy can "narrow" or "widen" outlines. 64 | ;; 65 | ;; Narrowing an outline hides all the outlines in the buffer except 66 | ;; for the outline at the cursor. This is useful when you want to 67 | ;; view or operate on the contents of a single outline. 68 | ;; 69 | ;; Press =N= to narrow to the outline for Exercise 1. 70 | ;; 71 | ;; Widening shows all outlines that have been hidden through 72 | ;; narrowing. 73 | ;; 74 | ;; Press =W= to widen the outlines. Notice how the rest of the 75 | ;; tutorial is displayed. 76 | ;; 77 | ;; Move to Exercise 2, show it, and narrow to it using the 78 | ;; shortcuts you've learned. 79 | ;; 80 | ;;** Exercise 2 81 | ;; Let's wrap up this lesson with one final shortcut. 82 | ;; 83 | ;; =2-I= will show the condensed outline structure for all the visible 84 | ;; outlines in the buffer. 85 | ;; 86 | ;; Press =W= to show the hidden outlines. 87 | ;; Press =2-I= and proceed to Lesson 1.2 88 | ;; 89 | ;;* Lesson 1.2: MOVING THE CURSOR 90 | ;; 91 | ;; Before we learn about moving the cursor, let's get some terminology 92 | ;; out the way: 93 | ;; 94 | ;; Def. 1: Point - The current cursor position. 95 | ;; 96 | ;; Def. 2: Opener - A term for the characters ( and [ and {. 97 | ;; 98 | ;; Def. 3: Closer - A term for the characters ) and ] and }. 99 | ;; 100 | ;; Def. 4: Short Binding - A command called by an uppercase or 101 | ;; lowercase letter. 102 | ;; 103 | ;; Def. 5: Special - A state the point can be in under certain 104 | ;; conditions. 105 | ;; 106 | ;; The point is 'special' when any of the conditions below are true: 107 | ;; 108 | ;; - the point is on an opener 109 | ;; - the point is on a closer 110 | ;; - the point is at the start of a comment 111 | ;; - the region is active 112 | ;; 113 | ;; When the point is special, lowercase and uppercase letters will 114 | ;; call short bindings instead of inserting characters. The digit 115 | ;; keys will call `digit-argument'. The behavior of these commands 116 | ;; can depend on the particular variation of special as described 117 | ;; above. 118 | ;; 119 | ;;** Exercise 1 120 | ;; 121 | ;; Let's test =f=, which is bound to `lispy-flow'. 122 | ;; 123 | ;; 1. Move into the special position: the first char of the line 124 | ;; indicated below. 125 | ;; 2. Press =f= to move the cursor to the next paren in current direction. 126 | ;; <--- special 127 | ;; 128 | ;; Hold =f= to move repeatedly until you reach "Skip a bit, Brother..." 129 | (with-output-to-string 130 | (defvar weapon "hand grenade") 131 | (let ((name "Saint Atilla")) 132 | (princ (format "And %s raised the %s up on high, saying, " name weapon))) 133 | (defun cite (str) 134 | (format "\"%s\"" str)) 135 | (princ (cite (concat 136 | "O Lord, bless this thy " 137 | weapon 138 | ", that with it thou mayst blow thine " 139 | "enemies to tiny bits, in thy mercy."))) 140 | (princ " And the Lord did grin. ") 141 | (princ "And the people did feast upon ") 142 | (princ (mapconcat (lambda (x) (format "the %ss" x)) 143 | '("lamb" "sloth" "carp" "anchovie" "orangutan" 144 | "breakfast cereal" "fruit bat") 145 | ", and ")) 146 | (princ ", and large chu...") 147 | (princ "\n\nSkip a bit, Brother...")) 148 | ;; 1. Now press =d= bound to `lispy-different' to switch to the different 149 | ;; side of the list. Press it a few times to get the feeling. 150 | ;; 151 | ;; 2. Making sure that you are at the right paren, press and hold =f= until 152 | ;; you reach "hand grenade". 153 | ;; 154 | ;; 3. Press =d= and hold =f=, making circles around the expressions until 155 | ;; you're comfortable. 156 | ;; 157 | ;; ** To move the cursor, press the =h=, =j=, =k=, =l= keys as indicated. ** 158 | ;; 159 | ;; ^ 160 | ;; k Hint: The =h= key is at the left and moves left. 161 | ;; < h l > The =l= key is at the right and moves right. 162 | ;; j The =j= key looks like a down arrow. 163 | ;; v 164 | ;; 165 | ;; Note that if it's impossible to move, the commands will fail silently. 166 | ;; In common code situations, if =j= doesn't work, it means that you've 167 | ;; reached the last expression of the parent list. The typical follow-up is 168 | ;; on =h= followed by either =j= or =k=. 169 | ;; 170 | ;; When on an outline, =j= and =k= are equivalent to =J= and =K=, respectively. 171 | ;; 172 | ;; Use your knowledge of =f= and =d= to setup the point in various places 173 | ;; and see what the arrow keys do there. Remember, the arrow keys will 174 | ;; behave differently depending on the side of the expression your cursor 175 | ;; is at. 176 | ;; 177 | ;; <--- To end the lesson, move the point here and press =W= (`widen'). 178 | ;;* Lesson 1.3: EXITING AND ENTERING SPECIAL 179 | ;; 180 | ;; There are a few ways to exit special. 181 | ;; If you're on an opener, closer, or comment character, just press 182 | ;; =C-f= or =C-n= or =C-p= or anything that will move your cursor away 183 | ;; from the special condition character. 184 | ;; 185 | ;;** Exercise 1: 186 | ;; 187 | ;; Let's quickly try exiting special. 188 | ;; 189 | ;; Hold =f= to move repeatedly until you reach "Skip a bit, Brother..." 190 | (with-output-to-string 191 | (defvar weapon "hand grenade") 192 | (let ((name "Saint Atilla")) 193 | (princ (format "And %s raised the %s up on high, saying, " name weapon))) 194 | (defun cite (str) 195 | (format "\"%s\"" str)) 196 | (princ (cite (concat 197 | "O Lord, bless this thy " 198 | weapon 199 | ", that with it thou mayst blow thine " 200 | "enemies to tiny bits, in thy mercy."))) 201 | (princ " And the Lord did grin. ") 202 | (princ "And the people did feast upon ") 203 | (princ (mapconcat (lambda (x) (format "the %ss" x)) 204 | '("lamb" "sloth" "carp" "anchovie" "orangutan" 205 | "breakfast cereal" "fruit bat") 206 | ", and ")) 207 | (princ ", and large chu...") 208 | (princ "\n\nSkip a bit, Brother...")) 209 | ;; Now press =C-f= to exit special mode. 210 | ;; You'll notice that pressing =f= now just self-inserts the character. 211 | ;; 212 | ;; Proceed to the next outline, LOCAL AND GLOBAL BINDINGS. 213 | ;; 214 | ;;** LOCAL AND GLOBAL BINDINGS 215 | ;; 216 | ;; You've seen how lispy's short bindings provide a concise way to 217 | ;; navigate lisp code, but they can only be used when the point is 218 | ;; special. These bindings are referred to as 'local'. 219 | ;; 220 | ;; Def. 6: Local - Bindings that only work in special. 221 | ;; 222 | ;; Lispy provides other bindings that can be used anytime when `lispy-mode' 223 | ;; is active. These bindings are referred to as 'global'. 224 | ;; 225 | ;; Def. 7: Global - Bindings that work anywhere, regardless of the 226 | ;; point location. 227 | ;; 228 | ;; Proceed to the next exercise. 229 | ;; 230 | ;;** Exercise 2: 231 | ;; 232 | ;; Let's experiment with some lispy globals. Below are some global 233 | ;; bindings that are particularly useful for entering special: 234 | ;; 235 | ;; - =[= calls `lispy-backward': when not in special, this command moves to the 236 | ;; left paren of the containing list. Otherwise, its behavior is similar to 237 | ;; the Emacs command `backward-list'. 238 | ;; 239 | ;; - =]= calls `lispy-forward': when not in special, this command moves to the 240 | ;; right paren of the containing list. Otherwise, its behavior is similar to 241 | ;; the Emacs command `forward-list'. 242 | ;; 243 | ;; - =M-m= calls `lispy-mark-symbol': This command marks the current symbol with 244 | ;; the region. When the region is already active, try to extend it by one 245 | ;; symbol. 246 | ;; 247 | ;; Hold =f= to move repeatedly until you reach "Skip a bit, Brother..." 248 | (with-output-to-string 249 | (defvar weapon "hand grenade") 250 | (let ((name "Saint Atilla")) 251 | (princ (format "And %s raised the %s up on high, saying, " name weapon))) 252 | (defun cite (str) 253 | (format "\"%s\"" str)) 254 | (princ (cite (concat 255 | "O Lord, bless this thy " 256 | weapon 257 | ", that with it thou mayst blow thine " 258 | "enemies to tiny bits, in thy mercy."))) 259 | (princ " And the Lord did grin. ") 260 | (princ "And the people did feast upon ") 261 | (princ (mapconcat (lambda (x) (format "the %ss" x)) 262 | '("lamb" "sloth" "carp" "anchovie" "orangutan" 263 | "breakfast cereal" "fruit bat") 264 | ", and ")) 265 | (princ ", and large chu...") 266 | (princ "\n\nSkip a bit, Brother...")) 267 | ;; Press =C-f= to exit special and move forward one character. 268 | ;; Edit the text in the string to your liking and then press =[= twice. 269 | ;; 270 | ;; Your point should be on the previous expression and it should be special. 271 | ;; Verify by moving around with =j= and =k=. 272 | ;; 273 | ;; Now navigate your point so you're on the expression with ", and large chu..." 274 | ;; Press =C-f= again to exit special. 275 | ;; Edit the string to your liking and then press =]= twice. 276 | ;; 277 | ;; Your point should now be at the end of the last expression. 278 | ;; 279 | ;; Take some time to experiment with =[= and =]= and the commands you've learned 280 | ;; previously. 281 | ;; 282 | ;; When you're finished, place your cursor back on "Skip a bit, Brother...". 283 | ;; Press =C-f= to exit special. 284 | ;; Press =M-m=. This should mark the current symbol `princ'. 285 | ;; 286 | ;; Marking will be covered in more detail later, but for now, take some time to 287 | ;; experiment with =M-m= then proceed to Lesson 1.3. 288 | ;; 289 | ;;* Lesson 1.4: LISP EDITING - DELETION 290 | ;; 291 | ;; To delete things, first you need to have a lot of them. 292 | ;; 293 | ;; 1. You can generate a lot of code by moving the point before 294 | ;; (with-output-to-string and pressing =c= bound to `lispy-clone' a 295 | ;; few times. 296 | ;; 297 | ;; 2. Now, press =C-d= bound to `lispy-delete' to delete the whole 298 | ;; sexp. Do this until you have only two top-level sexps left. 299 | ;; 300 | ;; 3. Press =f= to move inside the top-level sexp and hold =C-d= until 301 | ;; everything inside is deleted. Note how the top-level sexp is 302 | ;; deleted after all its children are gone. 303 | ;; 304 | ;; 4. Make another clone with =c= and switch to the different side 305 | ;; with =d=. Do a single =f= and practice pressing =DEL= bound to 306 | ;; `lispy-delete-backward'. 307 | ;; 308 | ;; 5. Use conventional means to position the point at either side of 309 | ;; the string and the quote and test what both =C-d= and =DEL= do. 310 | (with-output-to-string 311 | (defvar weapon "hand granade") 312 | (let ((name "Saint Atilla")) 313 | (princ (format "And %s raised the %s up on high, saying, " name weapon))) 314 | (defun cite (str) 315 | (format "\"%s\"" str)) 316 | (princ (cite (concat 317 | "O Lord, bless this thy " 318 | weapon 319 | ", that with it thou mayst blow thine " 320 | "enemies to tiny bits, in thy mercy."))) 321 | (princ " And the Lord did grin. ") 322 | (princ "And the people did feast upon ") 323 | (princ (mapconcat (lambda (x) (format "the %ss" x)) 324 | '("lamb" "sloth" "carp" "anchovie" "orangutan" 325 | "breakfast cereal" "fruit bat") 326 | ", and ")) 327 | (princ ", and large chu...") 328 | (princ "\n\nSkip a bit, Brother...")) 329 | ;;* Lesson 1.5: LISP EDITING - INSERTION 330 | ;; 331 | ;; Basic insertion bindings: 332 | ;; 333 | ;; =(= - inserts () and goes backward one char in code, self-inserts 334 | ;; in comments and strings. 335 | ;; ={= - inserts {} and goes backward one char everywhere 336 | ;; =}= - inserts [] and goes backward one char everywhere 337 | ;; ="= - inserts "" and goes backward one char, auto-quotes in strings 338 | ;; 339 | ;; All four of the pairs above will try to add once space where 340 | ;; appropriate, so to insert "(list ())", press =(list(=. 341 | ;; 342 | ;; If you want any of the mentined keys to self-insert once, remember 343 | ;; that prefixing any key with =C-q= will do that. 344 | ;; 345 | ;; All four pairs will wrap the current thing when the region is active. 346 | ;; All four pairs will wrap the current symbol when prefixed with =C-u=. 347 | ;; 348 | ;;* Lesson 1.6: LISP EDITING - APPENDING 349 | ;; 350 | ;; You can append the current list: 351 | ;; 352 | ;; - from the front with =2 SPC= 353 | ;; - from the back with =3 SPC= 354 | ;; - from the back with a newline =4 SPC= 355 | ;;* Lesson 1.7: OUTLINE NAVIGATION 356 | ;; 357 | ;; Here, several bindings depend on conditions additional to being in 358 | ;; special. Some of them are a superset of others, for example 359 | ;; COMMENT is a superset of OUTLINE, since all outlines are comments. 360 | ;; 361 | ;; When at COMMENT, you can: 362 | ;; 363 | ;; - navigate to the next outline with =j=, 364 | ;; - navigate to the previous outline with =k=. 365 | ;; - navigate to the first code paren of the outline with =f=. 366 | ;; 367 | ;; When at OUTLINE, you can: 368 | ;; 369 | ;; - fold and unfold the outline with =i=, 370 | ;; - promote the outline with =h=, 371 | ;; - demote the outline with =l=, 372 | ;; - recenter / undo recenter with =v=, 373 | ;; - create a new outline with =a=, 374 | ;; - move to the end of the oneline heading with =t=, 375 | ;; - narrow to the outline with =N=, 376 | ;; - widen with =W=. 377 | ;; 378 | ;; Anywhere, you can: 379 | ;; 380 | ;; - navigate to the next outline with =J=, 381 | ;; - navigate to the previous outline with =K=, 382 | ;; - toggle folding to all outlines of level 1 with =I=, 383 | ;; - get the table of contents with a prefix argument and =I=, e.g. =2I=, 384 | ;; 385 | ;; Also note that =I= has a global alias =S-TAB=, which you can use 386 | ;; even when not is special. It mirrors the popular `org-mode' 387 | ;; package, with the difference that it's a two-way toggle instead of 388 | ;; three-way. You can get the third option with a prefix argument. 389 | ;;* Lesson 1.8: MOVING THE REGION 390 | -------------------------------------------------------------------------------- /lispy-inline.el: -------------------------------------------------------------------------------- 1 | ;;; lispy-inline.el --- inline arglist and documentation. -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2014-2015 Oleh Krehel 4 | 5 | ;; This file is not part of GNU Emacs 6 | 7 | ;; This file is free software; you can redistribute it and/or modify 8 | ;; it under the terms of the GNU General Public License as published by 9 | ;; the Free Software Foundation; either version 3, or (at your option) 10 | ;; any later version. 11 | 12 | ;; This program is distributed in the hope that it will be useful, 13 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ;; GNU General Public License for more details. 16 | 17 | ;; For a full copy of the GNU General Public License 18 | ;; see . 19 | 20 | ;;; Commentary: 21 | ;; 22 | ;; Display current function arguments or docstring in an in-place 23 | ;; overlay. 24 | 25 | ;;; Code: 26 | 27 | (require 'thingatpt) 28 | 29 | (if (version< emacs-version "26.1") 30 | (progn 31 | (defsubst string-trim-left (string &optional regexp) 32 | "Trim STRING of leading string matching REGEXP. 33 | 34 | REGEXP defaults to \"[ \\t\\n\\r]+\"." 35 | (if (string-match (concat "\\`\\(?:" (or regexp "[ \t\n\r]+") "\\)") string) 36 | (replace-match "" t t string) 37 | string)) 38 | (defsubst string-trim-right (string &optional regexp) 39 | "Trim STRING of trailing string matching REGEXP. 40 | 41 | REGEXP defaults to \"[ \\t\\n\\r]+\"." 42 | (if (string-match (concat "\\(?:" (or regexp "[ \t\n\r]+") "\\)\\'") string) 43 | (replace-match "" t t string) 44 | string)) 45 | (defsubst string-trim (string &optional trim-left trim-right) 46 | "Trim STRING of leading and trailing strings matching TRIM-LEFT and TRIM-RIGHT. 47 | 48 | TRIM-LEFT and TRIM-RIGHT default to \"[ \\t\\n\\r]+\"." 49 | (string-trim-left (string-trim-right string trim-right) trim-left))) 50 | (require 'subr-x)) 51 | 52 | (defgroup lispy-faces nil 53 | "Font-lock faces for `lispy'." 54 | :group 'lispy 55 | :prefix "lispy-face-") 56 | 57 | (defface lispy-face-hint 58 | '((((class color) (background light)) 59 | :background "#fff3bc" :foreground "black") 60 | (((class color) (background dark)) 61 | :background "black" :foreground "#fff3bc")) 62 | "Basic hint face." 63 | :group 'lispy-faces) 64 | 65 | (defface lispy-face-req-nosel 66 | '((t (:inherit lispy-face-hint))) 67 | "Face for required unselected args." 68 | :group 'lispy-faces) 69 | 70 | (defface lispy-face-req-sel 71 | '((t (:inherit lispy-face-req-nosel :bold t))) 72 | "Face for required selected args." 73 | :group 'lispy-faces) 74 | 75 | (defface lispy-face-opt-nosel 76 | '((t (:inherit lispy-face-hint :slant italic))) 77 | "Face for optional unselected args." 78 | :group 'lispy-faces) 79 | 80 | (defface lispy-face-key-nosel 81 | '((t (:inherit lispy-face-hint :slant italic))) 82 | "Face for keyword unselected args." 83 | :group 'lispy-faces) 84 | 85 | (defface lispy-face-opt-sel 86 | '((t (:inherit lispy-face-opt-nosel :bold t))) 87 | "Face for optional selected args." 88 | :group 'lispy-faces) 89 | 90 | (defface lispy-face-key-sel 91 | '((t (:inherit lispy-face-opt-nosel :bold t))) 92 | "Face for keyword selected args." 93 | :group 'lispy-faces) 94 | 95 | (defface lispy-face-rst-nosel 96 | '((t (:inherit lispy-face-hint))) 97 | "Face for rest unselected args." 98 | :group 'lispy-faces) 99 | 100 | (defface lispy-face-rst-sel 101 | '((t (:inherit lispy-face-rst-nosel :bold t))) 102 | "Face for rest selected args." 103 | :group 'lispy-faces) 104 | 105 | (defcustom lispy-window-height-ratio 0.65 106 | "`lispy--show' will fail with string taller than window height times this. 107 | The caller of `lispy--show' might use a substitute e.g. `describe-function'." 108 | :type 'float 109 | :group 'lispy) 110 | 111 | (defvar lispy-elisp-modes 112 | '(emacs-lisp-mode lisp-interaction-mode eltex-mode minibuffer-inactive-mode 113 | suggest-mode) 114 | "Modes for which `lispy--eval-elisp' and related functions are appropriate.") 115 | 116 | (defvar lispy-clojure-modes 117 | '(clojure-mode clojurescript-mode clojurex-mode clojurec-mode) 118 | "Modes for which clojure related functions are appropriate.") 119 | 120 | (defvar lispy-overlay nil 121 | "Hint overlay instance.") 122 | 123 | (defvar lispy-hint-pos nil 124 | "Point position where the hint should be (re-) displayed.") 125 | 126 | (declare-function lispy--eval-clojure-cider "le-clojure") 127 | (declare-function lispy--clojure-args "le-clojure") 128 | (declare-function lispy--clojure-resolve "le-clojure") 129 | (declare-function lispy--describe-clojure-java "le-clojure") 130 | (declare-function lispy--eval-scheme "le-scheme") 131 | (declare-function lispy--eval-lisp "le-lisp") 132 | (declare-function lispy--lisp-args "le-lisp") 133 | (declare-function lispy--lisp-describe "le-lisp") 134 | (declare-function lispy--back-to-paren "lispy") 135 | (declare-function lispy--current-function "lispy") 136 | (declare-function lispy--in-comment-p "lispy") 137 | (declare-function lispy--bounds-string "lispy") 138 | 139 | ;; ——— Commands ———————————————————————————————————————————————————————————————— 140 | (defun lispy--back-to-python-function () 141 | "Move point from function call at point to the function name." 142 | (let ((pt (point)) 143 | bnd) 144 | (if (lispy--in-comment-p) 145 | (error "Not possible in a comment") 146 | (condition-case nil 147 | (progn 148 | (when (setq bnd (lispy--bounds-string)) 149 | (goto-char (car bnd))) 150 | (up-list -1)) 151 | (error (goto-char pt))) 152 | (unless (looking-at "\\_<") 153 | (re-search-backward "\\_<" (line-beginning-position)))))) 154 | 155 | (defun lispy-arglist-inline () 156 | "Display arglist for `lispy--current-function' inline." 157 | (interactive) 158 | (save-excursion 159 | (if (eq major-mode 'python-mode) 160 | (lispy--back-to-python-function) 161 | (lispy--back-to-paren)) 162 | (unless (and (prog1 (lispy--cleanup-overlay) 163 | (when (window-minibuffer-p) 164 | (window-resize (selected-window) -1))) 165 | (= lispy-hint-pos (point))) 166 | (cond ((memq major-mode lispy-elisp-modes) 167 | (let ((sym (intern-soft (lispy--current-function)))) 168 | (cond ((fboundp sym) 169 | (setq lispy-hint-pos (point)) 170 | (lispy--show (lispy--pretty-args sym)))))) 171 | ((or (memq major-mode '(cider-repl-mode)) 172 | (memq major-mode lispy-clojure-modes)) 173 | (require 'le-clojure) 174 | (setq lispy-hint-pos (point)) 175 | (lispy--show (lispy--clojure-args (lispy--current-function)))) 176 | 177 | ((eq major-mode 'lisp-mode) 178 | (require 'le-lisp) 179 | (setq lispy-hint-pos (point)) 180 | (lispy--show (lispy--lisp-args (lispy--current-function)))) 181 | 182 | ((eq major-mode 'python-mode) 183 | (require 'le-python) 184 | (setq lispy-hint-pos (point)) 185 | (let ((arglist (lispy--python-arglist 186 | (python-info-current-symbol) 187 | (buffer-file-name) 188 | (line-number-at-pos) 189 | (current-column)))) 190 | (while (eq (char-before) ?.) 191 | (backward-sexp)) 192 | (lispy--show arglist))) 193 | 194 | (t (error "%s isn't supported currently" major-mode)))))) 195 | 196 | (defvar lispy--di-window-config nil 197 | "Store window configuration before `lispy-describe-inline'.") 198 | 199 | (defun lispy--hint-pos () 200 | "Point position for the first column of the hint." 201 | (save-excursion 202 | (cond ((region-active-p) 203 | (goto-char (region-beginning))) 204 | ((eq major-mode 'python-mode) 205 | (condition-case nil 206 | (goto-char (beginning-of-thing 'sexp)) 207 | (error (up-list -1)))) 208 | (t 209 | (lispy--back-to-paren))) 210 | (point))) 211 | 212 | (defun lispy--cleanup-overlay () 213 | "Delete `lispy-overlay' if it's valid and return t." 214 | (when (overlayp lispy-overlay) 215 | (delete-overlay lispy-overlay) 216 | (setq lispy-overlay nil) 217 | t)) 218 | 219 | (declare-function geiser-doc-symbol-at-point "geiser-doc") 220 | 221 | (defun lispy--describe-inline (str pos) 222 | "Toggle the overlay hint." 223 | (condition-case nil 224 | (save-excursion 225 | (when (= 0 (count-lines (window-start) (point))) 226 | (recenter 1)) 227 | (setq lispy-hint-pos pos) 228 | (goto-char lispy-hint-pos) 229 | (lispy--show (propertize str 'face 'lispy-face-hint))) 230 | (error 231 | (lispy--cleanup-overlay)))) 232 | 233 | (declare-function cider-nrepl-op-supported-p "ext:cider-client") 234 | (declare-function cider-sync-request:info "ext:cider-client") 235 | (declare-function nrepl-dict-get "ext:nrepl-dict") 236 | 237 | (defun lispy--docstring (sym) 238 | "Get the docstring for SYM." 239 | (cond 240 | ((memq major-mode lispy-elisp-modes) 241 | (setq sym (intern-soft sym)) 242 | (cond ((fboundp sym) 243 | (or (documentation sym) 244 | "undocumented")) 245 | ((boundp sym) 246 | (or (documentation-property 247 | sym 'variable-documentation) 248 | "undocumented")) 249 | (t "unbound"))) 250 | ((eq major-mode 'clojurescript-mode) 251 | (let (info) 252 | (or 253 | (and (cider-nrepl-op-supported-p "info") 254 | (setq info (cider-sync-request:info sym)) 255 | (nrepl-dict-get info "doc")) 256 | (concat "No doc for " sym)))) 257 | ((or (memq major-mode lispy-clojure-modes) 258 | (memq major-mode '(cider-repl-mode))) 259 | (require 'le-clojure) 260 | (let ((rsymbol (lispy--clojure-resolve sym))) 261 | (string-trim-left 262 | (replace-regexp-in-string 263 | "^\\(?:-+\n\\|\n*.*$.*@.*\n*\\)" "" 264 | (cond ((stringp rsymbol) 265 | (lispy--eval-clojure-cider 266 | (format "(with-out-str (clojure.repl/doc %s))" rsymbol))) 267 | ((eq rsymbol 'special) 268 | (lispy--eval-clojure-cider 269 | (format "(with-out-str (clojure.repl/doc %s))" sym))) 270 | ((eq rsymbol 'keyword) 271 | "No docs for keywords") 272 | ((and (listp rsymbol) 273 | (eq (car rsymbol) 'variable)) 274 | (cadr rsymbol)) 275 | (t 276 | (or (lispy--describe-clojure-java sym) 277 | (format "Could't resolve '%s" sym)))))))) 278 | ((eq major-mode 'lisp-mode) 279 | (require 'le-lisp) 280 | (lispy--lisp-describe sym)) 281 | ((eq major-mode 'python-mode) 282 | (require 'le-python) 283 | (if sym 284 | (lispy--python-docstring sym) 285 | (require 'semantic) 286 | (semantic-mode 1) 287 | (let ((sym (semantic-ctxt-current-symbol))) 288 | (if sym 289 | (progn 290 | (setq sym (mapconcat #'identity sym ".")) 291 | (or 292 | (lispy--python-docstring sym) 293 | (progn 294 | (message "no doc: %s" sym) 295 | nil))) 296 | (error "The point is not on a symbol"))))) 297 | (t 298 | (format "%s isn't supported currently" major-mode)))) 299 | 300 | (declare-function semantic-ctxt-current-symbol "ctxt") 301 | 302 | (defun lispy-describe-inline () 303 | "Display documentation for `lispy--current-function' inline." 304 | (interactive) 305 | (cond ((cl-some 306 | (lambda (window) 307 | (equal (buffer-name (window-buffer window)) "*lispy-help*")) 308 | (window-list)) 309 | (when (window-configuration-p lispy--di-window-config) 310 | (set-window-configuration lispy--di-window-config))) 311 | ((eq major-mode 'scheme-mode) 312 | (geiser-doc-symbol-at-point)) 313 | (t 314 | (let ((new-hint-pos (lispy--hint-pos))) 315 | (if (and (eq lispy-hint-pos new-hint-pos) 316 | (overlayp lispy-overlay)) 317 | (lispy--cleanup-overlay) 318 | (lispy--describe-inline 319 | (lispy--docstring (lispy--current-function)) 320 | new-hint-pos)))))) 321 | 322 | (declare-function lispy--python-docstring "le-python") 323 | (declare-function lispy--python-arglist "le-python") 324 | (declare-function python-info-current-symbol "python") 325 | 326 | ;; ——— Utilities ——————————————————————————————————————————————————————————————— 327 | (defun lispy--arglist (symbol) 328 | "Get arglist for SYMBOL." 329 | (let (doc) 330 | (if (setq doc (help-split-fundoc (documentation symbol t) symbol)) 331 | (car doc) 332 | (prin1-to-string 333 | (cons symbol (help-function-arglist symbol t)))))) 334 | 335 | (defun lispy--join-pad (strs width) 336 | "Join STRS padding each line with WIDTH spaces." 337 | (let* ((maxw (apply #'max (mapcar #'length strs))) 338 | (padding (make-string width ?\ )) 339 | (fstring (format "%%- %ds" maxw))) 340 | (mapconcat 341 | (lambda (x) 342 | (concat 343 | padding 344 | (let ((str (format fstring x))) 345 | (font-lock-append-text-property 346 | 0 (length str) 'face 'lispy-face-hint str) 347 | str))) 348 | strs 349 | "\n"))) 350 | 351 | (defun lispy--show-fits-p (str) 352 | "Return nil if window isn't large enough to display STR whole." 353 | (let ((strs (split-string str "\n"))) 354 | (when (or (< (length strs) (* lispy-window-height-ratio (window-height))) 355 | (window-minibuffer-p)) 356 | strs))) 357 | 358 | (defun lispy--show (str) 359 | "Show STR hint when `lispy--show-fits-p' is t." 360 | (let ((last-point (point)) 361 | (strs (lispy--show-fits-p str))) 362 | (if strs 363 | (progn 364 | (setq str (lispy--join-pad 365 | strs 366 | (+ (if (window-minibuffer-p) 367 | (- (minibuffer-prompt-end) (point-min)) 368 | 0) 369 | (string-width (buffer-substring 370 | (line-beginning-position) 371 | (point)))))) 372 | (save-excursion 373 | (goto-char lispy-hint-pos) 374 | (if (= -1 (forward-line -1)) 375 | (setq str (concat str "\n")) 376 | (end-of-line) 377 | (setq str (concat "\n" str))) 378 | (setq str (concat str 379 | (buffer-substring (point) (1+ (point))))) 380 | (if lispy-overlay 381 | (progn 382 | (move-overlay lispy-overlay (point) (+ (point) 1)) 383 | (overlay-put lispy-overlay 'invisible nil)) 384 | (setq lispy-overlay (make-overlay (point) (+ (point) 1))) 385 | (overlay-put lispy-overlay 'priority 9999)) 386 | (overlay-put lispy-overlay 'display str) 387 | (overlay-put lispy-overlay 'after-string "") 388 | (put 'lispy-overlay 'last-point last-point))) 389 | (setq lispy--di-window-config (current-window-configuration)) 390 | (save-selected-window 391 | (pop-to-buffer (get-buffer-create "*lispy-help*")) 392 | (let ((inhibit-read-only t)) 393 | (delete-region (point-min) (point-max)) 394 | (insert str) 395 | (goto-char (point-min)) 396 | (help-mode)))))) 397 | 398 | (defun lispy--pretty-args (symbol) 399 | "Return a vector of fontified strings for function SYMBOL." 400 | (let* ((args (cdr (read (lispy--arglist symbol)))) 401 | (p-opt (cl-position '&optional args :test 'equal)) 402 | (p-rst (or (cl-position '&rest args :test 'equal) 403 | (cl-position-if (lambda (x) 404 | (and (symbolp x) 405 | (string-match 406 | "\\.\\.\\.\\'" 407 | (symbol-name x)))) 408 | args))) 409 | (a-req (cl-subseq args 0 (or p-opt p-rst (length args)))) 410 | (a-opt (and p-opt 411 | (cl-subseq args (1+ p-opt) (or p-rst (length args))))) 412 | (a-rst (and p-rst (last args)))) 413 | (format 414 | "(%s)" 415 | (mapconcat 416 | #'identity 417 | (append 418 | (list (propertize (symbol-name symbol) 'face 'lispy-face-hint)) 419 | (mapcar 420 | (lambda (x) 421 | (propertize (downcase (prin1-to-string x)) 'face 'lispy-face-req-nosel)) 422 | a-req) 423 | (mapcar 424 | (lambda (x) 425 | (propertize (downcase (prin1-to-string x)) 'face 'lispy-face-opt-nosel)) 426 | a-opt) 427 | (mapcar 428 | (lambda (x) 429 | (setq x (downcase (symbol-name x))) 430 | (unless (string-match "\\.\\.\\.$" x) 431 | (setq x (concat x "..."))) 432 | (propertize x 'face 'lispy-face-rst-nosel)) 433 | a-rst)) 434 | " ")))) 435 | 436 | (provide 'lispy-inline) 437 | 438 | ;;; Local Variables: 439 | ;;; outline-regexp: ";; ———" 440 | ;;; End: 441 | 442 | ;;; lispy-inline.el ends here 443 | -------------------------------------------------------------------------------- /lispy-clojure.clj: -------------------------------------------------------------------------------- 1 | ;;; lispy-clojure.clj --- lispy support for Clojure. 2 | 3 | ;; Copyright (C) 2015-2018 Oleh Krehel 4 | 5 | ;; This file is not part of GNU Emacs 6 | 7 | ;; This file is free software; you can redistribute it and/or modify 8 | ;; it under the terms of the GNU General Public License as published by 9 | ;; the Free Software Foundation; either version 3, or (at your option) 10 | ;; any later version. 11 | 12 | ;; This program is distributed in the hope that it will be useful, 13 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ;; GNU General Public License for more details. 16 | 17 | ;; For a full copy of the GNU General Public License 18 | ;; see . 19 | 20 | (ns lispy.clojure 21 | (:require [clojure.repl :as repl] 22 | [clojure.pprint] 23 | [clojure.java.io :as io] 24 | [clojure.string :as str] 25 | [clojure.tools.namespace.file :as nfile] 26 | [clojure.tools.namespace.find :as nf] 27 | [clojure.java.classpath :as cp]) 28 | (:import 29 | (java.io File LineNumberReader InputStreamReader 30 | PushbackReader FileInputStream) 31 | (java.util.jar JarFile) 32 | (clojure.lang RT))) 33 | 34 | (defmacro xcond 35 | "Common Lisp style `cond'. 36 | 37 | It's more structured than `cond', thus exprs that use it are lot more 38 | malleable to refactoring." 39 | [& clauses] 40 | (when clauses 41 | (let [clause (first clauses)] 42 | (if (= (count clause) 1) 43 | `(or ~(first clause) 44 | (xcond 45 | ~@(next clauses))) 46 | `(if ~(first clause) 47 | (do ~@(next clause)) 48 | (xcond 49 | ~@(next clauses))))))) 50 | 51 | (defn file-exists? [f] 52 | (. (io/file f) exists)) 53 | 54 | (defn expand-file-name [name dir] 55 | (. (io/file dir name) getCanonicalPath)) 56 | 57 | (defn expand-home 58 | [path] 59 | (if (.startsWith path "~") 60 | (let [sep (.indexOf path File/separator)] 61 | (str (io/file (System/getProperty "user.home") 62 | (subs path (inc sep))))) 63 | path)) 64 | 65 | (defn source-fn 66 | "Returns a string of the source code for the given symbol, if it can 67 | find it. This requires that the symbol resolve to a Var defined in 68 | a namespace for which the .clj is in the classpath. Returns nil if 69 | it can't find the source. 70 | 71 | Example: (source-fn 'filter)" 72 | [x] 73 | (let [v (resolve x) 74 | m (and v (meta v)) 75 | file (or (:l-file m) (:file m)) 76 | line (or (:l-line m) (:line m))] 77 | (when (and file line (> line 1)) 78 | (let [filepath (expand-home file) 79 | strm (or (.getResourceAsStream (RT/baseLoader) filepath) 80 | (FileInputStream. filepath))] 81 | (with-open [rdr (LineNumberReader. (InputStreamReader. strm))] 82 | (dotimes [_ (dec line)] (.readLine rdr)) 83 | (let [text (StringBuilder.) 84 | pbr (proxy [PushbackReader] [rdr] 85 | (read [] (let [i (proxy-super read)] 86 | (.append text (char i)) 87 | i)))] 88 | (if (= :unknown *read-eval*) 89 | (throw (IllegalStateException. "Unable to read source while *read-eval* is :unknown.")) 90 | (read (PushbackReader. pbr))) 91 | (str text))))))) 92 | 93 | (defn symbol-function 94 | "Return the source code for function SYM." 95 | [sym] 96 | (read-string 97 | (source-fn 98 | sym))) 99 | 100 | (defn macro? [x] 101 | (:macro (meta (resolve x)))) 102 | 103 | (defn arity [args] 104 | (if (some #{'&} args) 105 | 1000 106 | (count args))) 107 | 108 | (defn flatten-expr 109 | "Flatten a function call EXPR by substituting the arguments." 110 | [expr] 111 | (let [func-name (first expr) 112 | args (rest expr) 113 | func-def (symbol-function func-name) 114 | func-doc (when (string? (nth func-def 2)) 115 | (nth func-def 2)) 116 | func-rest (drop (if func-doc 3 2) func-def) 117 | func-rest (if (map? (first func-rest)) 118 | (rest func-rest) 119 | func-rest) 120 | func-bodies (if (vector? (first func-rest)) 121 | (list func-rest) 122 | func-rest) 123 | func-body (first (filter #(>= (arity (first %)) (count args)) 124 | (sort (fn [a b] (< (arity (first a)) 125 | (arity (first b)))) 126 | func-bodies))) 127 | func-args (first func-body) 128 | func-impl (rest func-body)] 129 | (cons 'let 130 | (cons (vec (if (some #{'&} [func-args]) 131 | (vector func-args (vec args)) 132 | (apply concat 133 | (filter (fn [[a b]] 134 | (not (= a b))) 135 | (partition 136 | 2 (interleave func-args args)))))) 137 | func-impl)))) 138 | 139 | (defn quote-maybe 140 | "Quote X that isn't self-quoting, like symbol or list." 141 | [x] 142 | (if (fn? x) 143 | x 144 | (if (or (symbol? x) 145 | (list? x)) 146 | (list 'quote x) 147 | x))) 148 | 149 | (defn dest 150 | "Transform `let'-style BINDINGS into a sequence of `def's." 151 | [bindings] 152 | (let [bs (partition 2 (destructure bindings)) 153 | as (filterv 154 | #(not (re-matches #"^(vec|map|seq|first)__.*" (name %))) 155 | (map first bs))] 156 | (concat '(do) 157 | (map (fn [[name val]] 158 | `(def ~name ~val)) 159 | bs) 160 | [(zipmap (map keyword as) as)]))) 161 | 162 | (defn get-func-args-defn [func-def n-args] 163 | (let [func-doc (when (string? (nth func-def 2)) 164 | (nth func-def 2)) 165 | func-rest (drop (if func-doc 3 2) func-def) 166 | func-rest (if (map? (first func-rest)) 167 | (rest func-rest) 168 | func-rest) 169 | func-bodies (if (vector? (first func-rest)) 170 | (list func-rest) 171 | func-rest) 172 | func-body (first (filter #(>= (arity (first %)) n-args) 173 | (sort (fn [a b] (< (arity (first a)) 174 | (arity (first b)))) 175 | func-bodies))) 176 | func-args (first func-body)] 177 | func-args)) 178 | 179 | (defn get-func-args-def [func-def n-args] 180 | (let [body (nth func-def 2)] 181 | (assert (= (first body) 'fn)) 182 | (let [args (first (filter vector? body)) 183 | args-count (count (vec (remove '#{& &form &env} args)))] 184 | (assert (or (= args-count n-args) 185 | (and (< args-count n-args) 186 | ((set args) '&)))) 187 | (vec (remove '#{&form &env} args))))) 188 | 189 | (defn get-func-args [func-def n-args] 190 | (xcond ((#{'defn 'defmacro} (first func-def)) 191 | (get-func-args-defn func-def n-args)) 192 | ((= (first func-def) 'def) 193 | (get-func-args-def func-def n-args)))) 194 | 195 | (defn shadow-map [] 196 | (or (ns-resolve *ns* 'shadows) 197 | (intern *ns* 'shadows {}))) 198 | 199 | (defn shadow-unmap [nspc] 200 | ;; (ns-unmap nspc 'shadows) 201 | (intern nspc 'shadows {})) 202 | 203 | (defmacro with-shadows [& forms] 204 | `(let ~(vec (mapcat (fn [[k _]] [(symbol k) `((shadow-map) ~k)]) 205 | (deref (shadow-map)))) 206 | ~@forms)) 207 | 208 | (defn shadow-def 209 | "Give SYM in *ns* shadow value EXPR. 210 | 211 | (with-shadows SYM) can be used to retrieve this value." 212 | [sym expr] 213 | (intern 214 | *ns* 215 | 'shadows 216 | (assoc (deref (shadow-map)) (name sym) expr))) 217 | 218 | (defn shadow-dest 219 | "Transform `let'-style BINDINGS into a sequence of `shadow-def's." 220 | ([bindings] 221 | (shadow-dest bindings *ns*)) 222 | ([bindings nspc] 223 | (let [[_do & forms] (dest bindings) 224 | [defs out] (partition-by map? forms)] 225 | `(let ~(vec (mapcat (fn [[_ n v]] [n v]) defs)) 226 | ~@(when (not= *ns* nspc) 227 | `((in-ns '~(ns-name nspc)))) 228 | ~@(map 229 | (fn [x] 230 | `(shadow-def '~(second x) ~(second x))) 231 | defs) 232 | ~@out)))) 233 | 234 | (defn debug-step-in 235 | "Evaluate the function call arugments and sub them into function arguments." 236 | [expr] 237 | (let [func-name (first expr) 238 | args (vec (rest expr)) 239 | func-def (symbol-function func-name) 240 | func-args (get-func-args func-def (count args)) 241 | func-ns (:ns (meta (resolve func-name))) 242 | eval-form (shadow-dest 243 | [func-args (if (macro? func-name) 244 | (list 'quote args) 245 | args)] 246 | func-ns)] 247 | (eval 248 | `(with-shadows 249 | ~eval-form)))) 250 | 251 | (defn object-methods [sym] 252 | (distinct 253 | (map #(.getName %) 254 | (xcond 255 | ((instance? java.lang.Class sym) 256 | (. sym getMethods)) 257 | 258 | ((instance? java.lang.Object sym) 259 | (. (type sym) getMethods)))))) 260 | 261 | (defn object-fields [sym] 262 | (map #(str "-" (.getName %)) 263 | (.getFields (type sym)))) 264 | 265 | (defmacro object-members [ob] 266 | `(with-shadows 267 | (concat (object-fields ~ob) 268 | (object-methods ~ob)))) 269 | 270 | (defn get-meth [obj method-name] 271 | (first (filter #(= (.getName %) method-name) 272 | (.getMethods (type obj))))) 273 | 274 | (defn method-signature [obj method-name] 275 | (str (get-meth obj method-name))) 276 | 277 | (defn get-ctors [obj] 278 | (. obj getDeclaredConstructors)) 279 | 280 | (defn format-ctor [s] 281 | (let [[_ name args] (re-find #"(?:public|protected) (.*)\((.*)\)" s)] 282 | (str name 283 | "." 284 | (if (= args "") 285 | "" 286 | (str " " (str/replace args #"," " ")))))) 287 | 288 | (defn ctor-args [sym] 289 | (str/join 290 | "\n" 291 | (map #(str "(" % ")") 292 | (map format-ctor 293 | (map str (get-ctors sym)))))) 294 | 295 | (defn resolve-sym [sym] 296 | (xcond 297 | [(symbol? sym) 298 | (if (special-symbol? sym) 299 | 'special 300 | (or 301 | (resolve sym) 302 | (first (keep #(ns-resolve % sym) (all-ns))) 303 | (when-let [val (try (load-string (str sym)) (catch Exception _e))] 304 | (list 'variable (str val)))))] 305 | 306 | [(keyword? sym) 'keyword] 307 | 308 | [:else 'unknown])) 309 | 310 | (defn class-name [cls] 311 | (str/replace (str cls) #"class " "")) 312 | 313 | (defn class-method-static? [method] 314 | (java.lang.reflect.Modifier/isStatic (.getModifiers method))) 315 | 316 | (defn class-methods [cname] 317 | (load-string (format "(.getMethods %s)" cname))) 318 | 319 | (defn find-method [sym] 320 | (let [[cname mname] (str/split (str sym) #"/") 321 | methods (->> 322 | (and cname 323 | (class-methods cname)) 324 | (filter #(= (.getName %) mname)))] 325 | (first methods))) 326 | 327 | (defn arglist [sym] 328 | (let [rsym (resolve-sym sym)] 329 | (xcond 330 | ((= 'special rsym) 331 | (->> (with-out-str 332 | (eval (list #'repl/doc sym))) 333 | (re-find #"\(.*\)") 334 | read-string rest 335 | (map str) 336 | (str/join " ") 337 | (format "[%s]") 338 | list)) 339 | ((and (nil? rsym) (re-find #"/" (str sym))) 340 | (let [method (find-method sym) 341 | args (->> method 342 | (.getParameterTypes) 343 | (map class-name) 344 | (str/join " "))] 345 | (format "(%s [%s]) -> %s" sym args 346 | (class-name (. method getReturnType))))) 347 | (:else 348 | (let [args (map str (:arglists (meta rsym)))] 349 | (if (empty? args) 350 | (condp #(%1 %2) (eval sym) 351 | map? "[key]" 352 | set? "[key]" 353 | vector? "[idx]" 354 | "is uncallable") 355 | args)))))) 356 | 357 | (defmacro ok 358 | "On getting an Exception, just print it." 359 | [& body] 360 | `(try 361 | (eval '~@body) 362 | (catch Exception ~'e (.getMessage ~'e)))) 363 | 364 | (defn classpath [] 365 | (map #(.getAbsolutePath (java.io.File. (.toURI %))) 366 | (.getURLs (java.lang.ClassLoader/getSystemClassLoader)))) 367 | 368 | (defn reader= 369 | "Equality accounting for reader-generated symbols." 370 | [a b] 371 | (try 372 | (xcond 373 | ((and (symbol? a) (symbol? b)) 374 | (or 375 | (= a b) 376 | (and 377 | (re-find #"[0-9]+#$" (name a)) 378 | (re-find #"[0-9]+#$" (name b)) 379 | true))) 380 | 381 | ((and (instance? java.util.regex.Pattern a) 382 | (instance? java.util.regex.Pattern b)) 383 | (= (. a toString) 384 | (. b toString))) 385 | 386 | ((and (empty? a) (empty? b)) 387 | true) 388 | 389 | (:else 390 | (and 391 | (reader= (first a) (first b)) 392 | (reader= (rest a) (rest b))))) 393 | (catch Exception _e 394 | (= a b)))) 395 | 396 | (defn position [x coll equality] 397 | (letfn [(iter [i coll] 398 | (xcond 399 | ((empty? coll) nil) 400 | ((equality x (first coll)) 401 | i) 402 | (:else 403 | (recur (inc i) (rest coll)))))] 404 | (iter 0 coll))) 405 | 406 | (defn guess-intent [expr context] 407 | (if (not (or (list? expr) 408 | (vector? expr))) 409 | expr 410 | (let [idx (position expr context reader=)] 411 | (xcond 412 | ((nil? idx) 413 | expr) 414 | 415 | ;; [x |(+ 1 2) y (+ 3 4)] => {:x 3} 416 | ((and (= (first context) 'let) (= idx 1)) 417 | (shadow-dest expr)) 418 | 419 | ((and (vector? context) 420 | (= 0 (rem (count context) 2)) 421 | (= 0 (rem (inc idx) 2)) 422 | (every? (some-fn symbol? vector? map?) (take-nth 2 context))) 423 | (shadow-dest 424 | (take 2 (drop (- idx 1) context)))) 425 | ((or (nil? context) 426 | (reader= expr context)) 427 | expr) 428 | ((and (#{'doseq 'for} (first context)) 429 | (vector? expr) 430 | (= 2 (count expr))) 431 | (shadow-dest 432 | [(first expr) (first (eval `(with-shadows ~(second expr))))])) 433 | ((and (#{'dotimes} (first context)) 434 | (vector? expr) 435 | (= 2 (count expr))) 436 | (shadow-dest 437 | [(first expr) 0])) 438 | ((#{'-> '->> 'doto} (first context)) 439 | (take (inc idx) context)) 440 | (:t 441 | expr))))) 442 | 443 | (defn add-location-to-defn [expr file line] 444 | (when (and (list? expr) 445 | (= 'defn (first expr)) 446 | file line) 447 | (let [arglist-pos (first (keep-indexed 448 | (fn [i x] (when (or (vector? x) (list? x)) 449 | i)) 450 | expr)) 451 | expr-head (take arglist-pos expr) 452 | expr-tail (drop arglist-pos expr) 453 | expr-doc (or (first (filter string? expr-head)) "") 454 | expr-map (or (first (filter map? expr-head)) {})] 455 | `(~'defn ~(nth expr 1) 456 | ~expr-doc 457 | ~(merge {:l-file file 458 | :l-line line} 459 | expr-map) 460 | ~@expr-tail)))) 461 | 462 | (defn add-location-to-def 463 | [[_def name & args] file line] 464 | (apply list 465 | _def 466 | (with-meta 467 | name 468 | {:l-file file 469 | :l-line line}) 470 | (if (> (count args) 1) 471 | args 472 | (cons "" args)))) 473 | 474 | (defn add-location-to-deflike [expr file line] 475 | (when (and file line (list? expr)) 476 | (xcond ((= (first expr) 'def) 477 | (add-location-to-def expr file line)) 478 | ((= (first expr) 'defn) 479 | (add-location-to-defn expr file line))))) 480 | 481 | (defn read-string-all 482 | "Read all objects from the string S." 483 | [s] 484 | (let [reader (java.io.PushbackReader. 485 | (java.io.StringReader. s))] 486 | (loop [res []] 487 | (if-let [x (try (read reader) 488 | (catch Exception _e))] 489 | (recur (conj res x)) 490 | res)))) 491 | 492 | (defn reval [e-str context-str & {:keys [file line]}] 493 | (let [expr (read-string e-str) 494 | context (try 495 | (read-string context-str) 496 | (catch Exception _)) 497 | full-expr (read-string (format "[%s]" e-str)) 498 | expr1 (xcond 499 | ((nil? context-str) 500 | (cons 'do full-expr)) 501 | ((= (count full-expr) 2) 502 | (shadow-dest full-expr)) 503 | ((add-location-to-deflike expr file line)) 504 | (:else 505 | (guess-intent expr context)))] 506 | (eval `(with-shadows 507 | (try 508 | (do ~expr1) 509 | (catch Exception ~'e 510 | (clojure.core/str "error: " ~ 'e))))))) 511 | 512 | (defn file->elisp [f] 513 | (if (file-exists? f) 514 | f 515 | (. (io/resource f) getPath))) 516 | 517 | (defonce ns-to-jar (atom {})) 518 | 519 | (defn ns-location [sym] 520 | (when (empty? @ns-to-jar) 521 | (reset! ns-to-jar 522 | (apply hash-map 523 | (->> 524 | (cp/classpath) 525 | (mapcat #(interleave 526 | (if (. % isFile) 527 | (nf/find-namespaces-in-jarfile (JarFile. %)) 528 | (nf/find-namespaces-in-dir % nil)) 529 | (repeat %))))))) 530 | (let [dir (get @ns-to-jar sym)] 531 | (if (. dir isFile) 532 | (let [jf (JarFile. dir) 533 | file-in-jar (first 534 | (filter 535 | (fn [f] 536 | (let [entry (nf/read-ns-decl-from-jarfile-entry jf f nil)] 537 | (when (and entry (= (first entry) sym)) 538 | f))) 539 | (nf/clojure-sources-in-jar jf)))] 540 | (list 541 | (str "file:" dir "!/" file-in-jar) 542 | 0)) 543 | (let [file-in-dir (first 544 | (filter 545 | (fn [f] 546 | (let [decl (nfile/read-file-ns-decl f nil)] 547 | (and decl (= (first decl) sym) 548 | f))) 549 | (nf/find-clojure-sources-in-dir dir)))] 550 | (list (.getCanonicalPath file-in-dir) 0))))) 551 | 552 | (defn location [sym] 553 | (let [rs (resolve sym) 554 | m (meta rs)] 555 | (xcond 556 | ((and (nil? rs) (ns-location sym))) 557 | ((:l-file m) 558 | (list (:l-file m) (:l-line m))) 559 | ((and (:file m) (not (re-matches #"^/tmp/" (:file m)))) 560 | (list (file->elisp (:file m)) (:line m)))))) 561 | 562 | (defn pp [expr] 563 | (with-out-str 564 | (clojure.pprint/pprint 565 | expr))) 566 | 567 | (defn all-docs [ns] 568 | (str/join 569 | "::" 570 | (->> (filter (fn [v] 571 | (and (var? v) 572 | (fn? (deref v)))) 573 | (vals (ns-map ns))) 574 | (map 575 | (fn [v] 576 | (let [m (meta v)] 577 | (str v "\n" (:arglists m) "\n" (:doc m)))))))) 578 | -------------------------------------------------------------------------------- /le-clojure.el: -------------------------------------------------------------------------------- 1 | ;;; le-clojure.el --- lispy support for Clojure. -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2014-2019 Oleh Krehel 4 | 5 | ;; This file is not part of GNU Emacs 6 | 7 | ;; This file is free software; you can redistribute it and/or modify 8 | ;; it under the terms of the GNU General Public License as published by 9 | ;; the Free Software Foundation; either version 3, or (at your option) 10 | ;; any later version. 11 | 12 | ;; This program is distributed in the hope that it will be useful, 13 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ;; GNU General Public License for more details. 16 | 17 | ;; For a full copy of the GNU General Public License 18 | ;; see . 19 | 20 | ;;; Commentary: 21 | ;; 22 | 23 | ;;; Code: 24 | 25 | ;;* Requires 26 | (require 'cider-client nil t) 27 | (require 'cider-connection nil t) 28 | (require 'cider-eval nil t) 29 | (require 'cider-find nil t) 30 | (require 'cider-debug nil t) 31 | 32 | (defcustom lispy-clojure-eval-method 'cider 33 | "REPL used for eval." 34 | :type '(choice 35 | (const :tag "CIDER" cider) 36 | (const :tag "UNREPL" spiral)) 37 | :group 'lispy) 38 | 39 | (defcustom lispy-cider-connect-method 'cider-jack-in 40 | "Function used to create a CIDER connection." 41 | :type '(choice 42 | (const cider-jack-in) 43 | (const cider-connect) 44 | (function :tag "Custom")) 45 | :group 'lispy) 46 | 47 | ;;* Namespace 48 | (defvar lispy--clojure-ns "user" 49 | "Store the last evaluated *ns*.") 50 | 51 | (defvar lispy--clojure-namespace-name-regex 52 | "^(\\(clojure.core/\\)?\\(in-\\)?ns\\+?[ 53 | [:space:]]+\\(?:\\(?:\\(#?\\^{[^}]*}\\)\\|\\(?:\\^:[^[:space:]]+\\)*\\)[ 54 | [:space:]]+\\)*[':]?\\([^\"()[:space:]]+\\_>\\)" 55 | "Store the obsoleted `clojure-namespace-name-regex'.") 56 | 57 | (defun lispy--clojure-detect-ns () 58 | "When there's only one (ns ...) in the buffer, use it." 59 | (save-excursion 60 | (goto-char (point-min)) 61 | (when (re-search-forward lispy--clojure-namespace-name-regex nil t) 62 | (let ((ns (match-string-no-properties 4))) 63 | (when (not (re-search-forward lispy--clojure-namespace-name-regex nil t)) 64 | (setq lispy--clojure-ns ns)))))) 65 | 66 | ;;* User wrapper for eval 67 | (defvar lispy--clojure-middleware-loaded-hash (make-hash-table :test #'equal) 68 | "Nil if the Clojure middleware in \"lispy-clojure.clj\" wasn't loaded yet.") 69 | 70 | (defun lispy--clojure-process-buffer () 71 | (if (or org-src-mode (eq major-mode 'org-mode)) 72 | (cadr (first (sesman--all-system-sessions 'CIDER))) 73 | (let ((cur-type (cider-repl-type-for-buffer))) 74 | (car (cider-repls cur-type nil))))) 75 | 76 | (defun lispy--clojure-middleware-loaded-p () 77 | (let ((conn (lispy--clojure-process-buffer))) 78 | (and conn (gethash conn lispy--clojure-middleware-loaded-hash)))) 79 | 80 | (defun lispy--clojure-babashka-p () 81 | (ignore-errors (cider--babashka-version))) 82 | 83 | (defun lispy--eval-clojure-context (e-str) 84 | (cond 85 | ((or (eq major-mode 'clojurescript-mode) 86 | (lispy--clojure-babashka-p)) 87 | e-str) 88 | ((string-match-p "#break" e-str) 89 | e-str) 90 | ((lispy--clojure-middleware-loaded-p) 91 | (let ((context-str 92 | (condition-case nil 93 | (let ((deactivate-mark nil)) 94 | (save-mark-and-excursion 95 | (lispy--out-backward 1 t) 96 | (deactivate-mark) 97 | (lispy--string-dwim))) 98 | (error "")))) 99 | (when (and (lispy--leftp) 100 | (looking-back "(for[ \t\n]*" (line-beginning-position -1))) 101 | (let* ((e-str-1 (save-excursion 102 | (forward-char 1) 103 | (forward-sexp 2) 104 | (lispy--string-dwim))) 105 | (coll (read (lispy--eval-clojure-1 106 | (format "(lispy.clojure/with-shadows (map str %s))" e-str-1) 107 | nil))) 108 | (idx (lispy--idx-from-list coll)) 109 | (sym (save-excursion 110 | (forward-char 1) 111 | (lispy--string-dwim)))) 112 | (setq e-str (format "%s (nth %s %d)" sym e-str-1 idx)))) 113 | (format (if (memq this-command '(special-lispy-eval 114 | special-lispy-eval-and-insert 115 | lispy-eval-current-outline)) 116 | "(lispy.clojure/pp (lispy.clojure/reval %S %S :file %S :line %S))" 117 | "(lispy.clojure/reval %S %S :file %S :line %S)") 118 | e-str 119 | context-str 120 | (buffer-file-name) 121 | (line-number-at-pos)))) 122 | (t 123 | e-str))) 124 | 125 | (defun lispy-eval-clojure (str) 126 | "Eval STR as a Clojure expression." 127 | (lispy--clojure-detect-ns) 128 | (if (eq lispy-clojure-eval-method 'spiral) 129 | (lispy--eval-clojure-spiral str) 130 | (lispy--eval-clojure-cider str))) 131 | 132 | ;;* Start REPL wrapper for eval 133 | (defvar lispy--clojure-hook-lambda nil 134 | "Store a lambda to call.") 135 | 136 | (defun lispy--clojure-eval-hook-lambda () 137 | "Call `lispy--clojure-hook-lambda'." 138 | (when lispy--clojure-hook-lambda 139 | (funcall lispy--clojure-hook-lambda) 140 | (setq lispy--clojure-hook-lambda nil)) 141 | (remove-hook 'nrepl-connected-hook 142 | 'lispy--clojure-eval-hook-lambda)) 143 | 144 | (defvar lispy-cider-jack-in-dependencies nil) 145 | 146 | (defvar cider-jack-in-cljs-dependencies) 147 | (defvar cider-jack-in-dependencies) 148 | 149 | (declare-function cider-connections "ext:cider-connection") 150 | (defvar cider-allow-jack-in-without-project) 151 | 152 | (defvar lispy-clojure-projects-alist nil 153 | "Use `cider-connect' instead of `cider-jack-in' for some projects. 154 | Each entry is (DIRECTORY :host HOSTNAME :port PORT). 155 | Example: '((\"~/git/luminous-1\" :host \"localhost\" :port 7000))") 156 | 157 | (defun lispy--clojure-middleware-load-hook () 158 | "Don't load the middleware too early for a ClojureScript REPL. 159 | It will cause an error, since before the init finishes it's a Clojure REPL." 160 | (unless (eq (lispy--clojure-process-type) 'cljs) 161 | (lispy--clojure-middleware-load))) 162 | 163 | (defun lispy--eval-clojure-cider (e-str) 164 | "Eval STR as Clojure code and return a string. 165 | Add the standard output to the result." 166 | (require 'cider) 167 | (let ((f-str (lispy--eval-clojure-context e-str)) 168 | deactivate-mark) 169 | (cond ((null (lispy--clojure-process-buffer)) 170 | (unless (eq major-mode 'clojurescript-mode) 171 | (setq lispy--clojure-hook-lambda 172 | `(lambda () 173 | (set-window-configuration 174 | ,(current-window-configuration)) 175 | (lispy--clojure-middleware-load) 176 | (lispy-message 177 | (lispy--eval-clojure-1 ,f-str ,e-str)))) 178 | (add-hook 'nrepl-connected-hook 179 | 'lispy--clojure-eval-hook-lambda t)) 180 | (let ((project-cfg (assoc (clojure-project-dir (cider-current-dir)) 181 | lispy-clojure-projects-alist))) 182 | (cond (project-cfg 183 | (cider-connect (cons :project-dir project-cfg)) 184 | "Using cider-connect") 185 | ((eq major-mode 'clojurescript-mode) 186 | (let ((cider-jack-in-cljs-dependencies nil)) 187 | (call-interactively #'cider-jack-in-cljs)) 188 | "Starting CIDER using cider-jack-in-cljs ...") 189 | (t 190 | (let ((cider-allow-jack-in-without-project t) 191 | (cider-jack-in-dependencies 192 | (delete-dups 193 | (append 194 | cider-jack-in-dependencies 195 | (and (eq major-mode 'clojure-mode) 196 | lispy-cider-jack-in-dependencies))))) 197 | (call-interactively lispy-cider-connect-method)) 198 | (format "Starting CIDER using %s ..." lispy-cider-connect-method))))) 199 | ((eq current-prefix-arg 7) 200 | (kill-new f-str)) 201 | ((and (eq current-prefix-arg 0) 202 | (lispy--eval-clojure-cider 203 | "(lispy.clojure/shadow-unmap *ns*)") 204 | nil)) 205 | (t 206 | (lispy--clojure-middleware-load) 207 | (lispy--eval-clojure-1 f-str e-str))))) 208 | 209 | ;;* Base eval 210 | (defun lispy--eval-clojure-1 (f-str e-str) 211 | (or 212 | (and (stringp e-str) 213 | (lispy--eval-clojure-handle-ns e-str)) 214 | (let* ((res (lispy--eval-nrepl-clojure f-str lispy--clojure-ns)) 215 | (status (nrepl-dict-get res "status")) 216 | (res (cond ((or (member "namespace-not-found" status)) 217 | (lispy--eval-nrepl-clojure f-str)) 218 | ((member "eval-error" status) 219 | (signal 'eval-error (lispy--clojure-pretty-string 220 | (nrepl-dict-get res "err")))) 221 | (t 222 | res))) 223 | (val 224 | (nrepl-dict-get res "value")) 225 | (out (nrepl-dict-get res "out"))) 226 | (when out 227 | (setq lispy-eval-output 228 | (concat (propertize out 'face 'font-lock-string-face) "\n"))) 229 | (if (string-match "\\`(lispy.clojure/\\(pp\\|reval\\)" f-str) 230 | (condition-case nil 231 | (string-trim (read val)) 232 | (error val)) 233 | (if (stringp val) 234 | (string-trim val)))))) 235 | 236 | (defun lispy--eval-clojure-handle-ns (str) 237 | (when (or (string-match "\\`(ns \\([a-z-_0-9\\.]+\\)" str) 238 | (string-match "\\`(in-ns '\\([a-z-_0-9\\.]+\\)" str)) 239 | (setq lispy--clojure-ns (match-string 1 str)) 240 | (let* ((res (lispy--eval-nrepl-clojure str "user")) 241 | (status (nrepl-dict-get res "status"))) 242 | (when (member "eval-error" status) 243 | (error (nrepl-dict-get res "err")))) 244 | lispy--clojure-ns)) 245 | 246 | ;;* Handle NREPL version incompat 247 | (defun lispy--eval-nrepl-clojure (str &optional namespace) 248 | (nrepl-sync-request:eval 249 | str 250 | (or (cider-current-connection) 251 | (lispy--clojure-process-buffer)) 252 | namespace)) 253 | 254 | (defvar spiral-conn-id) 255 | (defvar spiral-aux-sync-request-timeout) 256 | (declare-function spiral-projects-as-list "ext:spiral-project") 257 | (declare-function spiral-pending-eval-add "ext:spiral-project") 258 | (declare-function spiral-ast-unparse-to-string "ext:spiral-ast") 259 | (declare-function spiral-loop--send "ext:spiral-loop") 260 | 261 | (defun lispy--eval-clojure-spiral (str) 262 | (let* ((start (current-time)) 263 | (repl-buf (cdr (assoc :repl-buffer (car (spiral-projects-as-list))))) 264 | (conn-id (with-current-buffer repl-buf spiral-conn-id)) 265 | (unparse-no-properties 266 | (lambda (node) (substring-no-properties 267 | (spiral-ast-unparse-to-string node)))) 268 | stdout 269 | result) 270 | (spiral-loop--send conn-id :aux str) 271 | (spiral-pending-eval-add 272 | :aux conn-id 273 | :status :sent 274 | :eval-callback (lambda (eval-payload) 275 | (setq result (funcall unparse-no-properties eval-payload))) 276 | :stdout-callback (lambda (stdout-payload &rest _) 277 | (setq stdout 278 | (concat stdout 279 | (funcall unparse-no-properties stdout-payload))))) 280 | (while (and (not result) 281 | (not (input-pending-p)) ;; do not hang UI 282 | (or (not spiral-aux-sync-request-timeout) 283 | (< (cadr (time-subtract (current-time) start)) 284 | spiral-aux-sync-request-timeout))) 285 | (accept-process-output nil 0.01)) 286 | (if stdout 287 | (concat stdout "\n" result) 288 | result))) 289 | 290 | ;;* Rest 291 | (defun lispy--clojure-debug-quit () 292 | (interactive) 293 | (let ((pt (save-excursion 294 | (if (lispy--leftp) 295 | (forward-list) 296 | (lispy--out-forward 1)) 297 | (lispy-up 1) 298 | (lispy-different) 299 | (point))) 300 | (str (format "(do %s)" 301 | (mapconcat 302 | (lambda (x) 303 | (format "(lispy.clojure/shadow-def '%s %s)" (car x) (cadr x))) 304 | (nrepl-dict-get cider--debug-mode-response "locals") 305 | "\n")))) 306 | (catch 'exit 307 | (cider-debug-mode-send-reply ":quit")) 308 | (lispy--eval-clojure-1 str nil) 309 | (goto-char pt))) 310 | 311 | (when (boundp 'cider--debug-mode-map) 312 | (define-key cider--debug-mode-map "Z" 'lispy--clojure-debug-quit)) 313 | 314 | (defun lispy--clojure-resolve (symbol) 315 | "Return resolved SYMBOL. 316 | Return 'special or 'keyword appropriately. 317 | Otherwise try to resolve in current namespace first. 318 | If it doesn't work, try to resolve in all available namespaces." 319 | (let ((str (lispy--eval-clojure-cider 320 | (format "(lispy.clojure/resolve-sym '%s)" symbol)))) 321 | (cond 322 | ((string-match "^#'\\(.*\\)$" str) 323 | (match-string 1 str)) 324 | (t 325 | (read str))))) 326 | 327 | (defun lispy--clojure-symbol-to-args (symbol) 328 | (cond 329 | ((eq major-mode 'clojurescript-mode) 330 | (let (info) 331 | (and (cider-nrepl-op-supported-p "info") 332 | (setq info (cider-sync-request:info symbol)) 333 | (let ((args (nrepl-dict-get info "arglists-str"))) 334 | (if args 335 | (split-string args "\n") 336 | (nrepl-dict-get info "forms-str")))))) 337 | ((string= symbol ".") 338 | (lispy--clojure-dot-args)) 339 | ((string-match "\\`\\(.*\\)\\.\\'" symbol) 340 | (lispy--clojure-constructor-args (match-string 1 symbol))) 341 | (t 342 | (let ((sym (lispy--clojure-resolve symbol))) 343 | (cond 344 | ((eq sym 'special) 345 | (read 346 | (lispy--eval-clojure-cider 347 | (format "(lispy.clojure/arglist '%s)" symbol)))) 348 | ((eq sym 'keyword) 349 | (list "[map]")) 350 | ((eq sym 'undefined) 351 | (error "Undefined")) 352 | ((and (listp sym) (eq (car sym) 'variable)) 353 | (list "variable")) 354 | (t 355 | (read 356 | (lispy--eval-clojure-cider 357 | (format "(lispy.clojure/arglist '%s)" symbol))))))))) 358 | 359 | (defun lispy--clojure-args (symbol) 360 | "Return a pretty string with arguments for SYMBOL. 361 | Besides functions, handles specials, keywords, maps, vectors and sets." 362 | (let ((args (lispy--clojure-symbol-to-args symbol))) 363 | (if (listp args) 364 | (format 365 | "(%s %s)" 366 | (propertize symbol 'face 'lispy-face-hint) 367 | (mapconcat 368 | #'identity 369 | (mapcar (lambda (x) (propertize (downcase x) 370 | 'face 'lispy-face-req-nosel)) 371 | args) 372 | (concat "\n" 373 | (make-string (+ 2 (length symbol)) ?\ )))) 374 | (propertize args 'face 'lispy-face-hint)))) 375 | 376 | (defun lispy--describe-clojure-java (sym) 377 | "Return description for Clojure Java symol SYM." 378 | (read 379 | (lispy--eval-clojure-cider 380 | (format 381 | "(let [[_ cname mname] (re-find #\"(.*)/(.*)\" \"%s\") 382 | methods (and cname 383 | (try (load-string (format \"(.getMethods %%s)\" cname)) 384 | (catch Exception e))) 385 | methods (filter #(= (.getName %%) mname) methods)] 386 | (if (= 0 (count methods)) 387 | nil 388 | (clojure.string/join 389 | \"\\n\" (map (fn [m] (.toString m)) 390 | methods))))" 391 | sym)))) 392 | 393 | (defun lispy--clojure-macrop (symbol) 394 | "Test if SYMBOL is a macro." 395 | (equal (lispy--eval-clojure-cider 396 | (format "(:macro (meta #'%s))" symbol)) 397 | "true")) 398 | 399 | (defun lispy--clojure-middleware-unload () 400 | "Mark the Clojure middleware in \"lispy-clojure.clj\" as not loaded." 401 | (puthash (lispy--clojure-process-buffer) nil lispy--clojure-middleware-loaded-hash)) 402 | 403 | (defun lispy-cider-load-file (filename) 404 | (let ((ns-form (cider-ns-form))) 405 | (cider-map-repls :auto 406 | (lambda (connection) 407 | (when ns-form 408 | (cider-repl--cache-ns-form ns-form connection)) 409 | (cider-request:load-file (cider--file-string filename) 410 | (funcall cider-to-nrepl-filename-function 411 | (cider--server-filename filename)) 412 | (file-name-nondirectory filename) 413 | connection))))) 414 | 415 | (defcustom lispy-clojure-middleware-tests nil 416 | "When non-nil, run the tests from lispy-clojure.clj when loading it." 417 | :type 'boolean 418 | :group 'lispy) 419 | 420 | (defun lispy--clojure-process-type (&optional conn) 421 | (let ((conn (or conn (lispy--clojure-process-buffer)))) 422 | (if (string-match "(.*cljs" (buffer-name conn)) 423 | 'cljs 424 | 'clj))) 425 | 426 | (defun lispy--clojure-middleware-load () 427 | "Load the custom Clojure code in \"lispy-clojure.clj\"." 428 | (let* ((access-time (lispy--clojure-middleware-loaded-p)) 429 | (conn (lispy--clojure-process-buffer)) 430 | (conn-type (lispy--clojure-process-type conn)) 431 | (middleware-fname 432 | (expand-file-name 433 | (if (eq conn-type 'cljs) "lispy-clojure.cljs" "lispy-clojure.clj") 434 | lispy-site-directory)) 435 | (middleware-access-time (file-attribute-access-time 436 | (file-attributes middleware-fname)))) 437 | (when (or (null access-time) (time-less-p access-time middleware-access-time)) 438 | (setq lispy--clojure-ns "user") 439 | (unless (lispy--clojure-babashka-p) 440 | (save-window-excursion 441 | (lispy-cider-load-file 442 | (expand-file-name middleware-fname lispy-site-directory)))) 443 | (puthash conn middleware-access-time lispy--clojure-middleware-loaded-hash) 444 | (add-hook 'nrepl-disconnected-hook #'lispy--clojure-middleware-unload) 445 | (when (equal conn-type 'clj) 446 | (let ((test-fname (expand-file-name "lispy-clojure-test.clj" 447 | lispy-site-directory))) 448 | (when (and lispy-clojure-middleware-tests 449 | (file-exists-p test-fname)) 450 | (lispy-message 451 | (lispy--eval-clojure-cider (format "(load-file \"%s\")" test-fname))))))))) 452 | 453 | (defun lispy-flatten--clojure (_arg) 454 | "Inline a Clojure function at the point of its call." 455 | (let* ((begp (if (looking-at lispy-left) 456 | t 457 | (if (lispy-right-p) 458 | (progn (backward-list) 459 | nil) 460 | (lispy-left 1)))) 461 | (bnd (lispy--bounds-list)) 462 | (str (lispy--string-dwim bnd)) 463 | (expr (lispy--read str)) 464 | (result 465 | (if (and (symbolp (car expr)) 466 | (lispy--clojure-macrop (symbol-name (car expr)))) 467 | (lispy--eval-clojure-cider 468 | (format "(macroexpand '%s)" str)) 469 | (lispy--eval-clojure-cider 470 | (format "(lispy.clojure/flatten-expr '%s)" str))))) 471 | (goto-char (car bnd)) 472 | (delete-region (car bnd) (cdr bnd)) 473 | (insert result) 474 | (when begp 475 | (goto-char (car bnd)))) 476 | (lispy-alt-multiline)) 477 | 478 | (defun lispy--clojure-debug-step-in () 479 | "Inline a Clojure function at the point of its call." 480 | (lispy--clojure-detect-ns) 481 | (let* ((e-str (format "(lispy.clojure/debug-step-in\n'%s)" 482 | (lispy--string-dwim))) 483 | (str (substring-no-properties 484 | (lispy--eval-clojure-1 e-str nil))) 485 | (old-session (sesman-current-session 'CIDER))) 486 | (lispy-follow) 487 | (when (string-match "(clojure.core/in-ns (quote \\([^)]+\\))" str) 488 | (setq lispy--clojure-ns (match-string 1 str))) 489 | (when (equal (file-name-nondirectory (buffer-file-name)) "lispy-clojure.clj") 490 | (sesman-link-session 'CIDER old-session)) 491 | (lispy--eval-clojure-cider str) 492 | (lispy-flow 1))) 493 | 494 | (defun lispy-goto-line (line) 495 | (goto-char (point-min)) 496 | (forward-line (1- line))) 497 | 498 | (declare-function archive-zip-extract "arc-mode") 499 | 500 | (defun lispy-find-archive (archive path) 501 | (require 'arc-mode) 502 | (let ((name (format "%s:%s" archive path))) 503 | (switch-to-buffer 504 | (or (find-buffer-visiting name) 505 | (with-current-buffer (generate-new-buffer name) 506 | (archive-zip-extract archive path) 507 | (set-visited-file-name name) 508 | (setq-local default-directory (file-name-directory archive)) 509 | (setq-local buffer-read-only t) 510 | (set-buffer-modified-p nil) 511 | (set-auto-mode) 512 | (current-buffer)))))) 513 | 514 | (defun lispy-goto-symbol-clojure (symbol) 515 | "Goto SYMBOL." 516 | (lispy--clojure-detect-ns) 517 | (let* ((r (read (lispy--eval-clojure-cider 518 | (format "(lispy.clojure/location '%s)" symbol)))) 519 | (url (car r)) 520 | (line (cadr r)) 521 | archive) 522 | (cond 523 | ((file-exists-p url) 524 | (find-file url) 525 | (lispy-goto-line line)) 526 | ((and (string-match "\\`file:\\([^!]+\\)!/\\(.*\\)\\'" url) 527 | (file-exists-p (setq archive (match-string 1 url)))) 528 | (let ((path (match-string 2 url))) 529 | (lispy-find-archive archive path) 530 | (lispy-goto-line line))) 531 | (t 532 | (warn "unexpected: %S" symbol) 533 | (cider-find-var symbol))))) 534 | 535 | (defun lispy-goto-symbol-clojurescript (symbol) 536 | "Goto SYMBOL." 537 | (cider-find-var nil symbol)) 538 | 539 | (defun lispy--clojure-dot-object (&optional bnd) 540 | (let* ((bnd (or bnd 541 | (bounds-of-thing-at-point 'symbol) 542 | (cons (point) (point)))) 543 | (nested-p (eq (char-before (car bnd)) ?\())) 544 | (when (save-excursion (lispy--out-backward (if nested-p 2 1) t) (looking-at "(\\.+")) 545 | (string-trim 546 | (let ((str 547 | (concat 548 | (buffer-substring-no-properties (match-beginning 0) (1- (car bnd))) 549 | ")"))) 550 | (if (<= (save-excursion 551 | (when nested-p 552 | (lispy--out-backward 1 t)) 553 | (lispy-dotimes 100 (backward-sexp 1))) 554 | (if (or nested-p (= (car bnd) (cdr bnd))) 2 3)) 555 | (string-trim str "[(.]+" ")") 556 | str)))))) 557 | 558 | (defun lispy-clojure-complete-at-point () 559 | (cond ((lispy-complete-fname-at-point)) 560 | ((and (memq major-mode lispy-clojure-modes) 561 | (lispy--clojure-middleware-loaded-p)) 562 | (ignore-errors 563 | (lispy--clojure-detect-ns) 564 | (let* ((bnd (or (bounds-of-thing-at-point 'symbol) 565 | (cons (point) (point)))) 566 | (obj (lispy--clojure-dot-object bnd)) 567 | res) 568 | (cond ((and obj 569 | (setq res (lispy--eval-clojure-cider-noerror 570 | (format "(lispy.clojure/object-members %s)" obj)))) 571 | (let ((cands (read res))) 572 | (when (> (cdr bnd) (car bnd)) 573 | (setq cands (all-completions (lispy--string-dwim bnd) cands))) 574 | (list (car bnd) (cdr bnd) cands))) 575 | ((eq (lispy--clojure-process-type) 'cljs) 576 | nil))))))) 577 | 578 | (defun lispy--eval-clojure-cider-noerror (e-str) 579 | (condition-case nil 580 | (lispy--eval-clojure-cider e-str) 581 | (eval-error nil))) 582 | 583 | (defun lispy--clojure-dot-args () 584 | (save-excursion 585 | (lispy--back-to-paren) 586 | (let* ((object (save-mark-and-excursion 587 | (lispy-mark-list 2) 588 | (lispy--string-dwim))) 589 | (method (save-mark-and-excursion 590 | (lispy-mark-list 3) 591 | (lispy--string-dwim))) 592 | (sig (read 593 | (lispy--eval-clojure-cider 594 | (format "(lispy.clojure/method-signature (lispy.clojure/reval \"%s\" nil) \"%s\")" object method))))) 595 | (when (> (length sig) 0) 596 | (if (string-match "\\`public \\(.*\\)(\\(.*\\))\\'" sig) 597 | (let ((name (match-string 1 sig)) 598 | (args (match-string 2 sig))) 599 | (format "%s\n(. %s %s%s)" 600 | name object method 601 | (if (> (length args) 0) 602 | (concat " " args) 603 | ""))) 604 | sig))))) 605 | 606 | (defun lispy--clojure-constructor-args (symbol) 607 | (read (lispy--eval-clojure-cider 608 | (format "(lispy.clojure/ctor-args %s)" symbol)))) 609 | 610 | (defun lispy--clojure-pretty-string (str) 611 | "Return STR fontified in `clojure-mode'." 612 | (cond ((string-match "\\`\"error: \\([^\0]+\\)\"\\'" str) 613 | (concat (propertize "error: " 'face 'error) 614 | (match-string 1 str))) 615 | ((> (length str) 4000) 616 | str) 617 | (t 618 | (condition-case nil 619 | (with-temp-buffer 620 | (clojure-mode) 621 | (insert str) 622 | (lispy-font-lock-ensure) 623 | (buffer-string)) 624 | (error str))))) 625 | 626 | (defun lispy-clojure-apropos-action (s) 627 | (cider-doc-lookup 628 | (substring 629 | (car (split-string s "\\\\n")) 630 | 2))) 631 | 632 | (defun lispy-clojure-apropos () 633 | (interactive) 634 | (let ((cands 635 | (split-string (lispy--eval-clojure-cider 636 | "(lispy.clojure/all-docs 'clojure.core)") 637 | "::"))) 638 | (ivy-read "var: " cands 639 | :action #'lispy-clojure-apropos-action))) 640 | 641 | (provide 'le-clojure) 642 | 643 | ;;; le-clojure.el ends here 644 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License GPL 3][badge-license]](http://www.gnu.org/licenses/gpl-3.0.txt) 2 | [![Build Status](https://travis-ci.org/abo-abo/lispy.svg?branch=master)](https://travis-ci.org/abo-abo/lispy) 3 | [![Coverage Status](https://coveralls.io/repos/abo-abo/lispy/badge.svg?branch=master)](https://coveralls.io/r/abo-abo/lispy?branch=master) 4 | [![MELPA](http://melpa.org/packages/lispy-badge.svg)](http://melpa.org/#/lispy) 5 | [![MELPA Stable](http://stable.melpa.org/packages/lispy-badge.svg)](http://stable.melpa.org/#/lispy) 6 | 7 |

8 | lispy logo 10 |

11 | 12 | > short and sweet LISP editing 13 | 14 | 15 | **Table of Contents** 16 | 17 | - [Introduction](#introduction) 18 | - [Relation to vi](#relation-to-vi) 19 | - [Features](#features) 20 | - [Function reference](#function-reference) 21 | - [Getting Started](#getting-started) 22 | - [Installation instructions](#installation-instructions) 23 | - [via MELPA](#via-melpa) 24 | - [via el-get](#via-el-get) 25 | - [Configuration instructions](#configuration-instructions) 26 | - [Customization instructions](#customization-instructions) 27 | - [Operating on lists](#operating-on-lists) 28 | - [How to get into list-editing mode (special)](#how-to-get-into-list-editing-mode-special) 29 | - [Digit keys in special](#digit-keys-in-special) 30 | - [How to get out of special](#how-to-get-out-of-special) 31 | - [List commands overview](#list-commands-overview) 32 | - [Inserting pairs](#inserting-pairs) 33 | - [Reversible commands](#reversible-commands) 34 | - [Keys that modify whitespace](#keys-that-modify-whitespace) 35 | - [Command chaining](#command-chaining) 36 | - [Navigating with `avy`-related commands](#navigating-with-ace-jump-mode-related-commands) 37 | - [Operating on regions](#operating-on-regions) 38 | - [Ways to activate region](#ways-to-activate-region) 39 | - [Move region around](#move-region-around) 40 | - [Switch to the other side of the region](#switch-to-the-other-side-of-the-region) 41 | - [Grow/shrink region](#growshrink-region) 42 | - [Commands that operate on region](#commands-that-operate-on-region) 43 | - [IDE-like features](#ide-like-features) 44 | - [Demos](#demos) 45 | - [[Demo 1: Practice generating code](http://abo-abo.github.io/lispy/demo-1)](#demo-1-practice-generating-codehttpabo-abogithubiolispydemo-1) 46 | - [[Demo 2: The substitution model for procedure application](http://abo-abo.github.io/lispy/demo-2)](#demo-2-the-substitution-model-for-procedure-applicationhttpabo-abogithubiolispydemo-2) 47 | - [[Demo 3: Down the rabbit hole](http://abo-abo.github.io/lispy/demo-3)](#demo-3-down-the-rabbit-holehttpabo-abogithubiolispydemo-3) 48 | - [[Demo 4: Project Euler p100 and Clojure](http://abo-abo.github.io/lispy/demo-4)](#demo-4-project-euler-p100-and-clojurehttpabo-abogithubiolispydemo-4) 49 | - [[Demo 5: ->>ification](http://abo-abo.github.io/lispy/demo-5)](#demo-5--ificationhttpabo-abogithubiolispydemo-5) 50 | - [[Demo 6: cond->if->cond](http://abo-abo.github.io/lispy/demo-6)](#demo-6-cond-if-condhttpabo-abogithubiolispydemo-6) 51 | - [Screencasts](#screencasts) 52 | 53 | 54 | 55 | # Introduction 56 | 57 | This package reimagines Paredit - a popular method to navigate and 58 | edit LISP code in Emacs. 59 | 60 | The killer-feature are the short bindings: 61 | 62 | | command | binding | binding | command 63 | |:-----------------------------|:----------------:|:------------:|:------------------ 64 | |`paredit-forward` | C-M-f | j | `lispy-down` 65 | |`paredit-backward` | C-M-b | k | `lispy-up` 66 | |`paredit-backward-up` | C-M-u | h | `lispy-left` 67 | |`paredit-forward-up` | C-M-n | l | `lispy-right` 68 | |`paredit-raise-sexp` | M-r | r | `lispy-raise` 69 | |`paredit-convolute-sexp` | M-? | C | `lispy-convolute` 70 | |`paredit-forward-slurp-sexp` | C-) | > | `lispy-slurp` 71 | |`paredit-forward-barf-sexp` | C-} | < | `lispy-barf` 72 | |`paredit-backward-slurp-sexp` | C-( | > | `lispy-slurp` 73 | |`paredit-backward-barf-sexp` | C-{ | < | `lispy-barf` 74 | 75 | Most of more than 100 interactive commands that `lispy` provides are 76 | bound to a-z and A-Z in 77 | `lispy-mode`. You can see the full command reference with many 78 | examples [here](http://abo-abo.github.io/lispy/). 79 | 80 | The price for these short bindings is that they are only active when: 81 | 82 | - the point is before an open paren: `(`, `[` or `{` 83 | - the point is after a close paren: `)`, `]` or `}` 84 | - the region is active 85 | 86 | The advantage of short bindings is that you are more likely to use 87 | them. As you use them more, you learn how to combine them, increasing 88 | your editing efficiency. 89 | 90 | To further facilitate building complex commands from smaller commands, 91 | `lispy-mode` binds `digit-argument` to 0-9. For 92 | example, you can mark the third element of the list with 93 | 3m. You can then mark third through fifth element (three 94 | total) with 2> or >>. You can then move the 95 | selection to the last three elements of the list with 99j. 96 | 97 | If you are currently using Paredit, note that `lispy-mode` and 98 | `paredit-mode` can actually coexist with very few conflicts, although 99 | there would be some redundancy. 100 | 101 | ## Relation to vi 102 | 103 | The key binding method is influenced by vi, although this isn't modal 104 | editing *per se*. 105 | 106 | Here's a quote from Wikipedia on how vi works, in case you don't know: 107 | 108 | > vi is a modal editor: it operates in either insert mode (where typed 109 | > text becomes part of the document) or normal mode (where keystrokes 110 | > are interpreted as commands that control the edit session). For 111 | > example, typing i while in normal mode switches the editor to insert 112 | > mode, but typing i again at this point places an "i" character in 113 | > the document. From insert mode, pressing ESC switches the editor 114 | > back to normal mode. 115 | 116 | Here's an illustration of Emacs, vi and lispy bindings for inserting a 117 | char and calling a command: 118 | 119 | | | insert "j" | forward-list 120 | |------------------|:--------------:|:-------------: 121 | |Emacs | j | C-M-n 122 | |vi in insert mode | j | impossible 123 | |vi in normal mode | impossible | j 124 | |lispy | j | j 125 | 126 | Advantages/disadvantages: 127 | 128 | - Emacs can both insert and call commands without switching modes (since it has none), 129 | but the command bindings are long 130 | - vi has short command bindings, but you have to switch modes between inserting and calling commands 131 | - lispy has short command bindings and doesn't need to switch modes 132 | 133 | Of course it's not magic, lispy needs to have normal/insert mode to 134 | perform both functions with j. The difference from vi is 135 | that the mode is **explicit** instead of **implicit** - it's 136 | determined by the point position or the region state: 137 | 138 | - you are in normal mode when the point is before/after paren or the 139 | region is active 140 | - otherwise you are in insert mode 141 | 142 | So people who generally like Emacs bindings (like me) can have the 143 | cake and eat it too (no dedicated insert mode + shorter key bindings). 144 | While people who like vi can still get an experience that's reasonably 145 | close to vi for LISP editing (since vi's line-based approach isn't 146 | very appropriate for LISP anyway). 147 | 148 | But if you ask: 149 | 150 | > What if I want to insert when the point is before/after paren or the region is active? 151 | 152 | The answer is that because of the LISP syntax you don't want to write 153 | this: 154 | 155 | ```cl 156 | j(progn 157 | (forward-char 1))k 158 | ``` 159 | 160 | Also, Emacs does nothing special by default when the region is active 161 | and you press a normal key, so new commands can be called in that 162 | situation. 163 | 164 | ## Features 165 | 166 | - Basic navigation by-list and by-region: 167 | - h moves left 168 | - j moves down 169 | - k moves up 170 | - l moves right 171 | - f steps inside the list 172 | - b moves back in history for all above commands 173 | 174 | - Paredit transformations, callable by plain letters: 175 | - > slurps 176 | - < barfs 177 | - r raises 178 | - C convolutes 179 | - s moves down 180 | - w moves up 181 | - IDE-like features for Elisp, Clojure, Scheme, Common Lisp, Hy, Python and Julia: 182 | - e evals 183 | - E evals and inserts 184 | - g jumps to any tag in the current directory with semantic 185 | - G jumps to any tag in the current file 186 | - M-. jumps to symbol, M-, jumps back 187 | - F jumps to symbol, D jumps back 188 | - C-1 shows documentation in an overlay 189 | - C-2 shows arguments in an overlay 190 | - [Z](http://abo-abo.github.io/lispy/#lispy-edebug-stop) breaks 191 | out of `edebug`, while storing current function's arguments 192 | 193 | Some pictures [here](#ide-like-features). 194 | - Code manipulation: 195 | - i prettifies code (remove extra space, hanging parens ...) 196 | - xi transforms `cond` expression to equivalent `if` expressions 197 | - xc transforms `if` expressions to an equivalent `cond` expression 198 | - x> transforms expressions from/to an equivalent `thread-last` expression 199 | - xf flattens function or macro call (extract body and substitute arguments) 200 | - xr evals and replaces 201 | - xl turns current `defun` into a `lambda` 202 | - xd turns current `lambda` into a `defun` 203 | - O formats the code into one line 204 | - M formats the code into multiple lines 205 | - Misc. bindings: 206 | - outlines navigation/folding (J, K, I, i) 207 | - narrow/widen (N, W) 208 | - `ediff` (b, B) 209 | - `ert` (T) 210 | - `edebug` (xe) 211 | 212 | ## Function reference 213 | Most functions are cataloged and described at http://abo-abo.github.io/lispy/. 214 | 215 | # Getting Started 216 | ## Installation instructions 217 | ### via MELPA 218 | 219 | It's easiest/recommended to install from [MELPA](http://melpa.org/). 220 | Here's a minimal MELPA configuration for your `~/.emacs`: 221 | 222 | ```cl 223 | (package-initialize) 224 | (add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/")) 225 | ``` 226 | 227 | Afterwards, M-x package-install RET lispy RET (you might 228 | want to M-x package-refresh-contents RET beforehand if 229 | you haven't done so recently). 230 | 231 | ### via el-get 232 | 233 | [el-get](https://github.com/dimitri/el-get) also features a lispy recipe. 234 | Use M-x el-get-install RET lispy RET to install. 235 | 236 | ## Configuration instructions 237 | **Enable lispy automatically for certain modes** 238 | 239 | After installing, you can call M-x lispy-mode for any 240 | buffer with a LISP dialect source. To have `lispy-mode` activated 241 | automatically, use something like this: 242 | 243 | 244 | ```cl 245 | (add-hook 'emacs-lisp-mode-hook (lambda () (lispy-mode 1))) 246 | ``` 247 | 248 | **Enable lispy for `eval-expression`** 249 | 250 | Although I prefer to eval things in `*scratch*`, sometimes 251 | M-: - `eval-expression` is handy. Here's how to use lispy 252 | in the minibuffer during `eval-expression`: 253 | 254 | ```cl 255 | (defun conditionally-enable-lispy () 256 | (when (eq this-command 'eval-expression) 257 | (lispy-mode 1))) 258 | (add-hook 'minibuffer-setup-hook 'conditionally-enable-lispy) 259 | ``` 260 | 261 | ## Customization instructions 262 | 263 | If you want to replace some of the `lispy-mode`'s bindings you can do 264 | it like this: 265 | 266 | ```cl 267 | (eval-after-load "lispy" 268 | `(progn 269 | ;; replace a global binding with own function 270 | (define-key lispy-mode-map (kbd "C-e") 'my-custom-eol) 271 | ;; replace a global binding with major-mode's default 272 | (define-key lispy-mode-map (kbd "C-j") nil) 273 | ;; replace a local binding 274 | (lispy-define-key lispy-mode-map "s" 'lispy-down))) 275 | ``` 276 | 277 | ## Compatibility with other modes 278 | 279 | Use the `lispy-compat` variable to enable compatibility with modes that could otherwise conflict. These currently include: 280 | 281 | - god-mode 282 | - magit-blame-mode 283 | - edebug 284 | - cider 285 | - macrostep 286 | 287 | The default setting only enables compatibility with `edebug`. 288 | 289 | # Operating on lists 290 | 291 | ## How to get into list-editing mode (special) 292 | 293 | The plain keys will call commands when: 294 | - the point is positioned before paren 295 | - the point is positioned after paren 296 | - the region is active 297 | 298 | When one of the first two conditions is true, I say that the point is 299 | special. When the point is special, it's very clear to which sexp the 300 | list-manipulating command will be applied to, what the result be and 301 | where the point should end up afterwards. You can enhance this effect 302 | with `show-paren-mode` or similar. 303 | 304 | Here's an illustration to this effect, with `lispy-clone` (here, `|` 305 | represents the point): 306 | 307 | |before | key | after 308 | |:-------------------|:------------:|:----------------------- 309 | |`(looking-at "(")\|` | c | `(looking-at "(")` 310 | | | | `(looking-at "(")\|` 311 | 312 | |before | key | after 313 | |:-------------------|:------------:|:----------------------- 314 | |`\|(looking-at "(")` | c | `\|(looking-at "(")` 315 | | | | ` (looking-at "(")` 316 | 317 | You can use plain Emacs navigation commands to get into special, or you can use 318 | some of the dedicated commands: 319 | 320 | Key Binding | Description 321 | ----------------|----------------------------------------------------------- 322 | ] | `lispy-forward` - move to the end of the closest list, analogous to C-M-n (`forward-list`) 323 | [| `lispy-backward` - move to the start of the closest list, analogous to C-M-p (`backward-list`) 324 | C-3 | `lispy-right` - exit current list forwards, analogous to `up-list` 325 | ) | `lispy-right-nostring` exit current list forwards, but self-insert in strings and comments 326 | 327 | These are the few lispy commands that don't care whether the point is 328 | special or not. Other such bindings are DEL, C-d, C-k. 329 | 330 | Special is useful for manipulating/navigating lists. If you want to 331 | manipulate symbols, use [region selection](#operating-on-regions) 332 | instead. 333 | 334 | ## Digit keys in special 335 | 336 | When special, the digit keys call `digit-argument` which is very 337 | useful since most lispy commands accept a numeric argument. 338 | For instance, 3c is equivalent to ccc (clone sexp 3 times), and 339 | 4j is equivalent to jjjj (move point 4 sexps down). 340 | 341 | Some useful applications are 9l and 9h - they exit list forwards 342 | and backwards respectively at most 9 times which makes them 343 | effectively equivalent to `end-of-defun` and `beginning-of-defun`. Or 344 | you can move to the last sexp of the file with 999j. 345 | 346 | ## How to get out of special 347 | 348 | To get out of the special position, you can use any of the good-old 349 | navigational commands such as C-f or C-n. 350 | Additionally SPC will break out of special to get around the 351 | situation when you have the point between the open parens like this 352 | 353 | (|( 354 | 355 | and want to start inserting; SPC will change the code to 356 | this: 357 | 358 | (| ( 359 | 360 | ## List commands overview 361 | ### Inserting pairs 362 | 363 | Here's a list of commands for inserting [pairs](http://abo-abo.github.io/lispy/#lispy-pair): 364 | 365 | key | command 366 | ------------------|------------------------------------------------------------------- 367 | ( | [`lispy-parens`](http://abo-abo.github.io/lispy/#lispy-parens) 368 | { | [`lispy-braces`](http://abo-abo.github.io/lispy/#lispy-braces) 369 | } | [`lispy-brackets`](http://abo-abo.github.io/lispy/#lispy-brackets) 370 | " | [`lispy-quotes`](http://abo-abo.github.io/lispy/#lispy-quotes) 371 | 372 | ### Reversible commands 373 | 374 | A lot of Lispy commands come in pairs - one reverses the other: 375 | 376 | key | command | key | command 377 | ----------------|-------------------------------|----------------------------------|---------------------- 378 | j | `lispy-down` | k | `lispy-up` 379 | s | `lispy-move-down` | w | `lispy-move-up` 380 | > | `lispy-slurp` | < | `lispy-barf` 381 | c | `lispy-clone` | C-d or DEL | 382 | C | `lispy-convolute` | C | reverses itself 383 | d | `lispy-different` | d | reverses itself 384 | M-j | `lispy-split` | + | `lispy-join` 385 | O | `lispy-oneline` | M | `lispy-multiline` 386 | S | `lispy-stringify` | C-u " | `lispy-quotes` 387 | ; | `lispy-comment` | C-u ; | `lispy-comment` 388 | xi | `lispy-to-ifs` | xc | `lispy-to-cond` 389 | x> | `lispy-toggle-thread-last` | x> | reverses itself 390 | 391 | ### Keys that modify whitespace 392 | 393 | These commands handle whitespace in addition to inserting the expected 394 | thing. 395 | 396 | key | command 397 | ----------------|--------------------------- 398 | SPC | `lispy-space` 399 | : | `lispy-colon` 400 | ^ | `lispy-hat` 401 | C-m | `lispy-newline-and-indent` 402 | 403 | ### Command chaining 404 | 405 | Most special commands will leave the point special after they're 406 | done. This allows to chain them as well as apply them 407 | continuously by holding the key. Some useful hold-able keys are 408 | jkf<>cws;. 409 | Not so useful, but fun is /: start it from `|(` position and hold 410 | until all your Lisp code is turned into Python :). 411 | 412 | ### Navigating with `avy`-related commands 413 | 414 | key | command 415 | ----------------|-------------------------- 416 | q | `lispy-ace-paren` 417 | Q | `lispy-ace-char` 418 | a | `lispy-ace-symbol` 419 | H | `lispy-ace-symbol-replace` 420 | - | `lispy-ace-subword` 421 | 422 | q - `lispy-ace-paren` jumps to a "(" character within current 423 | top-level form (e.g. `defun`). It's much faster than typing in the 424 | `avy` binding + selecting "(", and there's less candidates, 425 | since they're limited to the current top-level form. 426 | 427 | a - `lispy-ace-symbol` will let you select which symbol to 428 | mark within current form. This can be followed up with e.g. eval, 429 | describe, follow, raise etc. Or you can simply m to 430 | deactivate the mark and edit from there. 431 | 432 | - - `lispy-ace-subword` is a niche command for a neat combo. Start with: 433 | 434 | (buffer-substring-no-properties 435 | (region-beginning)|) 436 | 437 | Type c, -, b and C-d to get: 438 | 439 | (buffer-substring-no-properties 440 | (region-beginning) 441 | (region-|)) 442 | 443 | Fill `end` to finish the statement. 444 | 445 | # Operating on regions 446 | Sometimes the expression that you want to operate on isn't bounded by parens. 447 | In that case you can mark it with a region and operate on that. 448 | 449 | ## Ways to activate region 450 | While in special: 451 | - Mark a sexp with m - `lispy-mark-list` 452 | - Mark a symbol within sexp a - `lispy-ace-symbol`. 453 | 454 | While not in special: 455 | - C-SPC - `set-mark-command` 456 | - mark a symbol at point with M-m - `lispy-mark-symbol` 457 | - mark containing expression (list or string or comment) with C-M-, - `lispy-mark` 458 | 459 | ## Move region around 460 | 461 | The arrow keys j/k will move the region up/down within the current 462 | list. The actual code will not be changed. 463 | 464 | ## Switch to the other side of the region 465 | 466 | Use d - `lispy-different` to switch between different sides 467 | of the region. The side is important since the grow/shrink operations 468 | apply to current side of the region. 469 | 470 | ## Grow/shrink region 471 | 472 | Use a combination of: 473 | - > - `lispy-slurp` - extend by one sexp from the current side. Use digit 474 | argument to extend by several sexps. 475 | - < - `lispy-barf` - shrink by one sexp from the current side. Use digit 476 | argument to shrink by several sexps. 477 | 478 | The other two arrow keys will mark the parent list of the current region: 479 | 480 | - h - `lispy-left` - mark the parent list with the point on the left 481 | - l - `lispy-right` - mark the parent list with the point on the right 482 | 483 | To do the reverse of the previous operation, i.e. to mark the first 484 | child of marked list, use i - `lispy-tab`. 485 | 486 | ## Commands that operate on region 487 | - m - `lispy-mark-list` - deactivate region 488 | - c - `lispy-clone` - clone region and keep it active 489 | - s - `lispy-move-down` - move region one sexp down 490 | - w - `lispy-move-up` - move region one sexp up 491 | - u - `lispy-undo` - deactivate region and undo 492 | - t - `lispy-teleport` - move region inside the sexp you select with `lispy-ace-paren` 493 | - C - `lispy-convolute` - exchange the order of application of two sexps that contain region 494 | - n - `lispy-new-copy` - copy region as kill without deactivating the mark 495 | - P - `lispy-paste` - replace region with current kill 496 | 497 | # IDE-like features 498 | 499 | These features are specific to the Lisp dialect used. Currently Elisp 500 | and Clojure (via `cider`) are supported. There's also basic 501 | evaluation support for: 502 | 503 | - Scheme (via `geiser`) 504 | - Common lisp (via `slime` or `sly`). 505 | - Hy (via `comint`). 506 | - Python (via `comint` and `jedi`). 507 | - Julia (via `julia-shell`). 508 | 509 | **`lispy-describe-inline`** 510 | 511 | Bound to C-1. Show the doc for the current function inline. 512 | 513 | C-h f is fine, but the extra buffer, and having to navigate to a symbol 514 | is tiresome. C-1 toggles on/off the inline doc for current function. 515 | No extra buffer necessary: 516 | 517 | ![screenshot](https://raw.github.com/abo-abo/lispy/master/images/doc-elisp.png) 518 | 519 | Here's how it looks for Clojure: 520 | 521 | ![screenshot](https://raw.github.com/abo-abo/lispy/master/images/doc-clojure.png) 522 | 523 | **`lispy-arglist-inline`** 524 | 525 | Bound to C-2. Show arguments for current function inline. 526 | 527 | `eldoc-mode` is cool, but it shows you arguments *over there* and 528 | you're writing *over here*!. No problem, C-2 fixes that: 529 | 530 | ![screenshot](https://raw.github.com/abo-abo/lispy/master/images/arglist-elisp.png) 531 | 532 | As you see, normal, &optional and &rest arguments have each a 533 | different face. Here's how it looks for Clojure: 534 | 535 | ![screenshot](https://raw.github.com/abo-abo/lispy/master/images/arglist-clojure.png) 536 | 537 | **`lispy-goto`** 538 | 539 | Bound to g. 540 | 541 | Use completion to select a symbol to jump to from all top-level symbols in the in current directory. 542 | 543 | Works out of the box for Elisp, Scheme and Common Lisp. 544 | [clojure-semantic](https://github.com/kototama/clojure-semantic) is 545 | required for Clojure. 546 | 547 | **`lispy-eval`** 548 | 549 | There's a feature similar to `ipython-notebook`. Evaluating an Emacs 550 | outline will evaluate all of the outline's code and echo the result of 551 | the last expression. When an outline ends with a colon (`:`), the 552 | result will instead be inserted into the buffer. If the evaluation 553 | result changes for whatever reason, it will be replaced after each 554 | subsequent e. 555 | 556 | Python, Clojure, and Julia currently have a slightly better notebook 557 | support, pressing e on the parent outline will evaluate all 558 | the children outlines sequentially. This allows to arrange scripts 559 | hierarchically, with relatively few top-level outlines and relatively 560 | many total outlines. Each outline's output can be examined by adding a 561 | `:` to the title of the outline. 562 | 563 | The following example shows a buffer before and after pressing e. 564 | 565 | ![lispy-python-notebook.png](https://raw.githubusercontent.com/wiki/abo-abo/lispy/images/lispy-python-notebook.png) 566 | 567 | There is one top-level outline, with one level-2 child, which in turn 568 | has a four level-3 children. Three of these children end in `:`, so 569 | their output will be updated after the eval. 570 | 571 | # Demos 572 | 573 | ## [Demo 1: Practice generating code](http://abo-abo.github.io/lispy/demo-1) 574 | ## [Demo 2: The substitution model for procedure application](http://abo-abo.github.io/lispy/demo-2) 575 | ## [Demo 3: Down the rabbit hole](http://abo-abo.github.io/lispy/demo-3) 576 | ## [Demo 4: Project Euler p100 and Clojure](http://abo-abo.github.io/lispy/demo-4) 577 | ## [Demo 5: ->>ification](http://abo-abo.github.io/lispy/demo-5) 578 | ## [Demo 6: cond->if->cond](http://abo-abo.github.io/lispy/demo-6) 579 | 580 | # Screencasts 581 | 582 | - The older stuff can be found on [vimeo](http://vimeo.com/user24828177/videos). 583 | - The newer stuff is on https://www.youtube.com/user/abo5abo/videos. 584 | 585 | [badge-license]: https://img.shields.io/badge/license-GPL_3-green.svg 586 | -------------------------------------------------------------------------------- /lispy-python.py: -------------------------------------------------------------------------------- 1 | # lispy-python.py --- lispy support for Python. 2 | 3 | # Copyright (C) 2016-2019 Oleh Krehel 4 | 5 | # This file is not part of GNU Emacs 6 | 7 | # This file is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 3, or (at your option) 10 | # any later version. 11 | 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | 17 | # For a full copy of the GNU General Public License 18 | # see . 19 | 20 | #* Imports 21 | import ast 22 | import collections 23 | import importlib 24 | import inspect 25 | import io 26 | import json 27 | import os 28 | import pprint as pp 29 | import re 30 | import shlex 31 | import subprocess 32 | import sys 33 | from ast import AST 34 | from contextlib import redirect_stdout 35 | from typing import List, Dict, Any, Union, Tuple, Optional, TypedDict, Callable, cast 36 | from types import TracebackType, MethodType, FunctionType, ModuleType, FrameType 37 | 38 | def sh(cmd: str) -> str: 39 | r = subprocess.run( 40 | shlex.split(cmd), 41 | stdout=subprocess.PIPE, 42 | stderr=subprocess.PIPE, 43 | encoding="utf-8", 44 | check=True) 45 | return r.stdout.strip() 46 | 47 | try: 48 | import reprlib 49 | repr1 = reprlib.Repr() 50 | repr1.maxlist = 10 51 | repr1.maxstring = 200 52 | except: 53 | pass 54 | try: 55 | import jedi 56 | except: 57 | try: 58 | pyenv_version = sh("pyenv global") 59 | pyversion = ".".join(pyenv_version.split(".")[:-1]) 60 | site_packages = os.path.expanduser(f"~/.pyenv/versions/{pyenv_version}/lib/python{pyversion}/site-packages/") 61 | sys.path.append(site_packages) 62 | import jedi 63 | except: 64 | print("Failed to load jedi. Some features won't work") 65 | 66 | #* Classes 67 | class Stack: 68 | line_numbers: Dict[Tuple[str, str], int] = {} 69 | 70 | def __init__(self, tb: Optional[TracebackType]): 71 | self.stack = [] 72 | self.stack_idx = 0 73 | while tb: 74 | name = tb.tb_frame.f_code.co_name 75 | fname = tb.tb_frame.f_code.co_filename 76 | if (fname, name) in Stack.line_numbers: 77 | lineno = Stack.line_numbers[(fname, name)] 78 | else: 79 | lineno = tb.tb_frame.f_lineno 80 | self.stack.append((fname, lineno, tb.tb_frame)) 81 | tb = tb.tb_next 82 | self.stack_top = len(self.stack) - 1 83 | if self.stack_top >= 0: 84 | self.set_frame(self.stack_top) 85 | 86 | def frame_string(self, i: int) -> str: 87 | (fname, line, f) = self.stack[i] 88 | res = " File \"%s\", line %d, Frame [%d/%d] (%s):" % ( 89 | f.f_code.co_filename, line, i, self.stack_top, f.f_code.co_name) 90 | return res 91 | 92 | def __repr__(self) -> str: 93 | frames = [] 94 | for i in range(self.stack_top + 1): 95 | s = self.frame_string(i) 96 | if i == self.stack_idx: 97 | s += "*" 98 | frames.append(s) 99 | return "\n".join(frames) 100 | 101 | def set_frame(self, i: int) -> None: 102 | if i >= 0: 103 | f = self.stack[i][2] 104 | self.stack_idx = i 105 | tf = top_level() 106 | tf.f_globals["lnames"] = f.f_locals.keys() 107 | for (k, v) in f.f_locals.items(): 108 | tf.f_globals[k] = v 109 | for (k, v) in f.f_globals.items(): 110 | tf.f_globals[k] = v 111 | 112 | print(self.frame_string(self.stack_idx)) 113 | 114 | def up(self, delta: int = 1) -> None: 115 | if self.stack_idx <= 0: 116 | if self.stack: 117 | print(self.frame_string(self.stack_idx)) 118 | else: 119 | self.stack_idx = max(self.stack_idx - delta, 0) 120 | self.set_frame(self.stack_idx) 121 | 122 | def down(self, delta: int = 1) -> None: 123 | if self.stack_idx >= self.stack_top: 124 | if self.stack: 125 | print(self.frame_string(self.stack_idx)) 126 | else: 127 | self.stack_idx = min(self.stack_idx + delta, self.stack_top) 128 | self.set_frame(self.stack_idx) 129 | 130 | class Autocall: 131 | def __init__(self, f: Callable): 132 | self.f = f 133 | 134 | def __call__(self, n: Any) -> None: 135 | self.f(n) 136 | 137 | def __repr__(self) -> str: 138 | try: 139 | self.f() 140 | except: 141 | pass 142 | return "" 143 | 144 | #* Functions 145 | def get_import_name(fname: str) -> str: 146 | for p in sys.path: 147 | if p == "": 148 | continue 149 | if fname.startswith(p): 150 | return fname[len(p) + 1:].partition(".")[0].replace("/", ".") 151 | return os.path.splitext(os.path.basename(fname))[0] 152 | 153 | def chfile(f: str) -> None: 154 | tf = top_level() 155 | tf.f_globals["__file__"] = f 156 | name = get_import_name(f) 157 | tf.f_globals["__name__"] = name 158 | d = os.path.dirname(f) 159 | try: 160 | os.chdir(d) 161 | if "sys" not in tf.f_globals: 162 | tf.f_globals["sys"] = importlib.import_module("sys") 163 | if name not in tf.f_globals["sys"].modules: 164 | try: 165 | mod = importlib.import_module(name) 166 | tf.f_globals["sys"].modules[name] = mod 167 | except: 168 | pass 169 | except: 170 | raise 171 | 172 | def arglist(sym: Callable) -> List[str]: 173 | def format_arg(arg_pair: Tuple[str, Optional[str]]) -> str: 174 | name, default_value = arg_pair 175 | if default_value: 176 | return name + " = " + default_value 177 | else: 178 | return name 179 | arg_info = inspect.getfullargspec(sym) 180 | if "self" in arg_info.args: 181 | arg_info.args.remove("self") 182 | if arg_info.defaults: 183 | defaults: List[Optional[str]] = [None] * (len(arg_info.args) - len(arg_info.defaults)) 184 | defaults += [repr(x) for x in arg_info.defaults] 185 | args = [format_arg(x) for x in zip(arg_info.args, defaults)] 186 | else: 187 | args = arg_info.args 188 | if arg_info.varargs: 189 | args += arg_info.varargs 190 | keywords = arg_info.kwonlydefaults 191 | if keywords: 192 | for k, v in keywords.items(): 193 | args.append(f"{k} = {v}") 194 | return args 195 | 196 | def print_elisp(obj: Any, end: str = "\n") -> None: 197 | if hasattr(obj, "_asdict") and obj._asdict is not None: 198 | if hasattr(type(obj), "__repr__"): 199 | print('"' + str(obj).replace('"', '') + '"') 200 | return 201 | # namedtuple 202 | try: 203 | print_elisp(obj._asdict(), end) 204 | except: 205 | print('"' + str(obj) + '"') 206 | elif hasattr(obj, "__array__"): 207 | # something that converts to a numpy array 208 | print_elisp(list(obj.__array__())) 209 | elif isinstance(obj, enumerate): 210 | print("(") 211 | for (i, v) in list(obj): 212 | print("(", end="") 213 | print_elisp(v, end="") 214 | print(")") 215 | print(")") 216 | elif isinstance(obj, set): 217 | print("(") 218 | for v in obj: 219 | print_elisp(v, end=" ") 220 | print(")") 221 | elif isinstance(obj, dict): 222 | print("(") 223 | for (k, v) in obj.items(): 224 | print(" :" + k, end=" ") 225 | print_elisp(v, end="\n") 226 | print(")") 227 | elif isinstance(obj, collections.abc.ItemsView): 228 | print("(") 229 | for (k, v) in obj: 230 | print_elisp((k, v), end="\n") 231 | print(")") 232 | elif isinstance(obj, collections.abc.KeysView): 233 | print_elisp(list(obj)) 234 | elif isinstance(obj, int): 235 | print(obj) 236 | elif isinstance(obj, list) or isinstance(obj, tuple): 237 | print("(", end="") 238 | for x in obj: 239 | print_elisp(x) 240 | print(")") 241 | else: 242 | if obj is not None: 243 | if type(obj) is str: 244 | # quote strings? 245 | # print("\"'" + re.sub("\"", "\\\"", obj) + "'\"", end=" ") 246 | print('"' + re.sub("\"", "\\\"", obj) + '"', end=" ") 247 | elif hasattr(obj, "to_dict"): 248 | print_elisp(obj.to_dict()) 249 | else: 250 | print('"' + repr(obj) + '"', end=" ") 251 | else: 252 | print('nil', end=end) 253 | 254 | def argspec(sym): 255 | arg_info = inspect.getfullargspec(sym) 256 | if arg_info: 257 | di = arg_info._asdict() 258 | fn = sym.__init__ if type(sym) is type else sym 259 | try: 260 | filename = fn.__code__.co_filename 261 | di["filename"] = filename 262 | if hasattr(sym, "__self__"): 263 | # bound method 264 | qname = sym.__self__.__class__.__name__ + "." + sym.__name__ 265 | else: 266 | qname = sym.__qualname__ 267 | tu = (filename, qname) 268 | if tu in Stack.line_numbers: 269 | di["line"] = Stack.line_numbers[tu] 270 | else: 271 | di["line"] = fn.__code__.co_firstlineno 272 | except AttributeError: 273 | m = sys.modules[sym.__module__] 274 | filename = m.__file__ 275 | nodes = ast.parse(open(filename).read()).body 276 | for node in nodes: 277 | if (type(node) in [ast.ClassDef, ast.FunctionDef] and 278 | node.name == sym.__name__): 279 | di["filename"] = filename 280 | di["line"] = node.lineno 281 | 282 | print_elisp(di) 283 | else: 284 | print("nil") 285 | 286 | def arglist_jedi(line, column, filename): 287 | script = jedi.Script(path=filename) 288 | defs = script.get_signatures(line, column) 289 | if defs: 290 | return [x.name for x in defs[0].params] 291 | else: 292 | return [] 293 | 294 | def jedi_completions(line): 295 | script = jedi.Script(code=line) 296 | return [x.name for x in script.complete()] 297 | 298 | def jedi_file_completions(fname, line, column): 299 | script = jedi.Script(path=fname) 300 | return [x.name for x in script.complete(line, column)] 301 | 302 | def is_assignment(code): 303 | ops = ast.parse(code).body 304 | return len(ops) == 1 and type(ops[0]) is ast.Assign 305 | 306 | def top_level(): 307 | """Return the topmost frame.""" 308 | f = sys._getframe() 309 | while f.f_back: 310 | f = f.f_back 311 | if f.f_code.co_filename == "": 312 | return f 313 | return f 314 | 315 | def list_step(varname, lst): 316 | f_globals = top_level().f_globals 317 | try: 318 | val = f_globals[varname] 319 | i = (lst.index(val) + 1) % len(lst) 320 | except: 321 | i = 0 322 | val = lst[i] 323 | print("[{}/{}]".format(i + 1, len(lst))) 324 | f_globals[varname] = val 325 | return val 326 | 327 | def argv(cmd: str) -> None: 328 | sys.argv = shlex.split(cmd) 329 | 330 | def find_global_vars(class_name): 331 | """Find global variables of type CLASS_NAME.""" 332 | return [(k, v) for (k, v) in top_level().f_globals.items() if v.__class__.__name__ == class_name] 333 | 334 | def rebind(method, fname=None, line=None): 335 | """Rebind METHOD named like Class.function in all top level instances of Class. 336 | 337 | Modifying a method is two-step: 338 | 1. eval the method as if it's a free top-level function, 339 | 2. modify all instances of the class with an adapter to this top-level function. 340 | """ 341 | qname = method.__qualname__ 342 | (cls_name, fun_name) = qname.split(".") 343 | for (n, v) in find_global_vars(cls_name): 344 | print("rebind:", n) 345 | top_level().f_globals[n].__dict__[fun_name] = MethodType(top_level().f_globals[fun_name], v) 346 | if fname and line: 347 | Stack.line_numbers[(fname, qname)] = line 348 | 349 | def pm() -> None: 350 | """Post mortem: recover the locals and globals from the last traceback.""" 351 | if hasattr(sys, 'last_traceback'): 352 | stack = Stack(sys.last_traceback) 353 | else: 354 | stack = Stack(sys.exc_info()[2]) 355 | tl = top_level() 356 | tl.f_globals["up"] = Autocall(stack.up) 357 | tl.f_globals["dn"] = Autocall(stack.down) 358 | globals()["stack"] = stack 359 | 360 | def pprint(x: Any) -> None: 361 | r1 = repr(x) 362 | if len(r1) > 1000 and repr1: 363 | print(repr1.repr(x)) 364 | else: 365 | if type(x) == collections.OrderedDict: 366 | print("{" + ",\n ".join([str(k) + ": " + str(v) for (k, v) in x.items()]) + "}") 367 | else: 368 | pp.PrettyPrinter(width=200).pprint(x) 369 | 370 | def to_str(x: Any) -> str: 371 | with io.StringIO() as buf, redirect_stdout(buf): 372 | pprint(x) 373 | return buf.getvalue().strip() 374 | 375 | def step_in(fn, *args): 376 | spec = inspect.getfullargspec(fn) 377 | f_globals = top_level().f_globals 378 | for (arg_name, arg_val) in zip(spec.args, args): 379 | f_globals[arg_name] = arg_val 380 | 381 | def step_into_module_maybe(module): 382 | if isinstance(module, FunctionType): 383 | try: 384 | module = sys.modules[module.__module__] 385 | except: 386 | pass 387 | elif getattr(module, "__module__", None): 388 | if module.__module__ == "__repl__": 389 | return 390 | module = sys.modules[module.__module__] 391 | if inspect.ismodule(module): 392 | tf = top_level() 393 | for (k, v) in module.__dict__.items(): 394 | if not re.match("^__", k): 395 | print(k) 396 | tf.f_globals[k] = v 397 | 398 | def slurp(fname: str) -> str: 399 | """Return `fname' contents as text.""" 400 | with open(fname, "r", encoding="utf-8") as fh: 401 | return fh.read() 402 | 403 | def definitions(path): 404 | (_, ext) = os.path.splitext(path) 405 | if ext == ".yml": 406 | return yaml_definitions(path) 407 | script = jedi.Script(slurp(path), path=path) 408 | res = [] 409 | for x in script.get_names(): 410 | if (x.get_definition_start_position()[0] == x.get_definition_end_position()[0] 411 | and "import" in x.get_line_code()): 412 | continue 413 | if x.type == "function": 414 | try: 415 | desc = x.description + "(" + ", ".join(p.name for p in x.params) + ")" 416 | except: 417 | desc = x.description 418 | res.append([desc, x.line]) 419 | elif x.type == "module": 420 | res.append(["import " + x.name, x.line]) 421 | elif x.type == "class": 422 | res.append([x.description, x.line]) 423 | try: 424 | members = x.defined_names() 425 | except: 426 | members = [] 427 | for m in members: 428 | res.append([x.name + "." + m.name, m.line]) 429 | else: 430 | res.append([x.description, x.line]) 431 | return res 432 | 433 | 434 | def yaml_definitions(path): 435 | res = [] 436 | ls = slurp(path).strip().splitlines() 437 | prev = "" 438 | symbol = "(\\w|[-_])+" 439 | for (i, line) in enumerate(ls, 1): 440 | if m := re.match(f"^({symbol})", line): 441 | res.append([m.group(1), i]) 442 | prev = m.group(1) + "." 443 | elif m := re.match(f"^ ({symbol})", line): 444 | res.append([prev + m.group(1), i]) 445 | return res 446 | 447 | 448 | def get_completions_readline(text): 449 | completions = [] 450 | completer = None 451 | try: 452 | import readline 453 | # pylint: disable=unused-import 454 | import rlcompleter 455 | completer = readline.get_completer() 456 | if getattr(completer, 'PYTHON_EL_WRAPPED', False): 457 | completer.print_mode = False 458 | i = 0 459 | while True: 460 | completion = completer(text, i) 461 | if not completion: 462 | break 463 | i += 1 464 | if not re.match("[0-9]__", completion): 465 | completions.append(completion) 466 | except: 467 | pass 468 | finally: 469 | if getattr(completer, 'PYTHON_EL_WRAPPED', False): 470 | completer.print_mode = True 471 | return [re.sub("__t__.", "", c) for c in completions] 472 | 473 | def get_completions(text): 474 | completions = get_completions_readline(text) 475 | if completions: 476 | return sorted(completions) 477 | m = re.match(r"([^.]+)\.(.*)", text) 478 | if m: 479 | (obj, part) = m.groups() 480 | regex = re.compile("^" + part) 481 | o = top_level().f_globals[obj] 482 | items = list(o.__dict__.keys()) if hasattr(o, "__dict__") else [] 483 | items += list(type(o).__dict__.keys()) if hasattr(type(o), "__dict__") else [] 484 | for x in set(items): 485 | if re.match(regex, x): 486 | if not x.startswith("_") or part.startswith("_"): 487 | completions.append(x) 488 | return sorted(completions) 489 | else: 490 | return [] 491 | 492 | def __PYTHON_EL_native_completion_setup(): 493 | import readline 494 | try: 495 | import __builtin__ 496 | except ImportError: 497 | # Python 3 498 | import builtins as __builtin__ 499 | 500 | builtins = dir(__builtin__) 501 | is_ipython = ('__IPYTHON__' in builtins or 502 | '__IPYTHON__active' in builtins) 503 | 504 | class __PYTHON_EL_Completer: 505 | '''Completer wrapper that prints candidates to stdout. 506 | 507 | It wraps an existing completer function and changes its behavior so 508 | that the user input is unchanged and real candidates are printed to 509 | stdout. 510 | 511 | Returned candidates are '0__dummy_completion__' and 512 | '1__dummy_completion__' in that order ('0__dummy_completion__' is 513 | returned repeatedly until all possible candidates are consumed). 514 | 515 | The real candidates are printed to stdout so that they can be 516 | easily retrieved through comint output redirect trickery. 517 | ''' 518 | 519 | PYTHON_EL_WRAPPED = True 520 | 521 | def __init__(self, completer): 522 | self.completer = completer 523 | self.last_completion = None 524 | self.print_mode = True 525 | 526 | def __call__(self, text, state): 527 | if state == 0: 528 | # Set the first dummy completion. 529 | self.last_completion = None 530 | completion = '0__dummy_completion__' 531 | else: 532 | completion = self.completer(text, state - 1) 533 | 534 | if not completion: 535 | if self.last_completion != '1__dummy_completion__': 536 | # When no more completions are available, returning a 537 | # dummy with non-sharing prefix allow ensuring output 538 | # while preventing changes to current input. 539 | # Coincidentally it's also the end of output. 540 | completion = '1__dummy_completion__' 541 | elif completion.endswith('('): 542 | # Remove parens on callables as it breaks completion on 543 | # arguments (e.g. str(Ari)). 544 | completion = completion[:-1] 545 | self.last_completion = completion 546 | 547 | if completion in ( 548 | '0__dummy_completion__', '1__dummy_completion__'): 549 | return completion 550 | elif completion: 551 | # For every non-dummy completion, return a repeated dummy 552 | # one and print the real candidate so it can be retrieved 553 | # by comint output filters. 554 | if self.print_mode: 555 | print(completion) 556 | return '0__dummy_completion__' 557 | else: 558 | return completion 559 | else: 560 | return completion 561 | 562 | completer = readline.get_completer() 563 | 564 | if not completer: 565 | # Used as last resort to avoid breaking customizations. 566 | # pylint: disable=unused-import 567 | import rlcompleter 568 | completer = readline.get_completer() 569 | 570 | if completer and not getattr(completer, 'PYTHON_EL_WRAPPED', False): 571 | # Wrap the existing completer function only once. 572 | new_completer = __PYTHON_EL_Completer(completer) 573 | if not is_ipython: 574 | readline.set_completer(new_completer) 575 | else: 576 | # Try both initializations to cope with all IPython versions. 577 | # This works fine for IPython 3.x but not for earlier: 578 | readline.set_completer(new_completer) 579 | # IPython<3 hacks readline such that `readline.set_completer` 580 | # won't work. This workaround injects the new completer 581 | # function into the existing instance directly: 582 | instance = getattr(completer, 'im_self', completer.__self__) 583 | instance.rlcomplete = new_completer 584 | 585 | if readline.__doc__ and 'libedit' in readline.__doc__: 586 | raise Exception('''libedit based readline is known not to work, 587 | see etc/PROBLEMS under \"In Inferior Python mode, input is echoed\".''') 588 | 589 | readline.parse_and_bind('tab: complete') 590 | # Require just one tab to send output. 591 | readline.parse_and_bind('set show-all-if-ambiguous on') 592 | 593 | 594 | __PYTHON_EL_native_completion_setup() 595 | 596 | def setup(init_file=None): 597 | sys.modules['__repl__'] = sys.modules[__name__] 598 | tl = top_level() 599 | tl.f_globals["__name__"] = "__repl__" 600 | tl.f_globals["pm"] = Autocall(pm) 601 | if init_file and os.path.exists(init_file): 602 | try: 603 | exec(open(init_file).read(), tl.f_globals) 604 | except: 605 | pass 606 | 607 | def reload(): 608 | import importlib.util 609 | spec = importlib.util.spec_from_file_location('lispy-python', __file__) 610 | mod = importlib.util.module_from_spec(spec) 611 | spec.loader.exec_module(mod) 612 | top_level().f_globals["lp"] = mod 613 | sys._getframe().f_back.f_globals["lp"] = mod 614 | sys._getframe().f_back.f_locals["lp"] = mod 615 | return mod 616 | 617 | def reload_module(fname): 618 | to_reload = [] 619 | for (name, module) in sys.modules.copy().items(): 620 | try: 621 | if module.__dict__.get("__file__") == fname and name != "__main__": 622 | to_reload.append((name, module)) 623 | except: 624 | pass 625 | for (name, module) in to_reload: 626 | try: 627 | importlib.reload(module) 628 | except: 629 | pass 630 | 631 | def goto_definition(fname: str, line: int, column: int) -> None: 632 | d = jedi.Script(path=fname).goto(line, column) 633 | if d: 634 | print_elisp((str(d[0].module_path), d[0].line, d[0].column)) 635 | 636 | def goto_link_definition(c: str) -> None: 637 | module = c.split(".")[0] 638 | d = jedi.Script(code=f"import {module}\n{c}").goto(2, len(c) - 2, follow_imports=True) 639 | if d: 640 | print_elisp((str(d[0].module_path), d[0].line, d[0].column)) 641 | 642 | def ast_pp(code: str) -> str: 643 | parsed = ast.parse(code, mode="exec") 644 | return ast.dump(parsed.body[0], indent=4) 645 | 646 | Expr = Union[List[ast.stmt], ast.stmt, Any] 647 | 648 | def has_return(p: Expr) -> bool: 649 | if isinstance(p, list): 650 | return any(has_return(x) for x in p) 651 | if isinstance(p, ast.Return): 652 | return True 653 | elif isinstance(p, ast.If): 654 | return has_return(p.body) or has_return(p.orelse) 655 | else: 656 | return False 657 | 658 | def tr_returns(p: Expr) -> Expr: 659 | if isinstance(p, list): 660 | return [tr_returns(x) for x in p] 661 | if isinstance(p, ast.Return) and p.value: 662 | return ast.parse("return locals() | {'__return__': " + ast.unparse(p.value) + "}").body[0] 663 | elif isinstance(p, ast.If): 664 | return ast.If( 665 | test=p.test, 666 | body=tr_returns(p.body), 667 | orelse=tr_returns(p.orelse)) 668 | else: 669 | return p 670 | 671 | def ast_call(func: Union[str, AST], args: List[Any] = [], keywords: List[Any] = []): 672 | if isinstance(func, str): 673 | func = ast.Name(func) 674 | return ast.Call(func=func, args=args, keywords=keywords) 675 | 676 | def wrap_return(parsed: List[ast.stmt]) -> Expr: 677 | return [ 678 | ast.FunctionDef( 679 | name="__res__", 680 | body=[*parsed, *ast.parse("return {'__return__': None}").body], 681 | decorator_list=[], 682 | args=[], 683 | lineno=0, 684 | col_offset=0), 685 | ast.Expr( 686 | ast_call( 687 | ast.Attribute(value=ast_call("locals"), attr="update"), 688 | args=[ast_call("__res__")])), 689 | ast.Expr(value=ast.Name("__return__")) 690 | ] 691 | 692 | def try_in_expr(p: Expr) -> Optional[Tuple[ast.expr, ast.expr]]: 693 | if not isinstance(p, list): 694 | return None 695 | if pytest_mark := try_pytest_mark(p): 696 | return pytest_mark 697 | p0 = p[0] 698 | if not isinstance(p0, ast.Expr): 699 | return None 700 | if not isinstance(p0.value, ast.Compare): 701 | return None 702 | if not isinstance(p0.value.ops[0], ast.In): 703 | return None 704 | return (p0.value.left, p0.value.comparators[0]) 705 | 706 | def select_item(code: str, idx: int, _f: Optional[FrameType] = None) -> Any: 707 | _f = _f or sys._getframe().f_back 708 | parsed = ast.parse(code, mode="exec").body 709 | in_expr = try_in_expr(parsed) 710 | assert in_expr 711 | (left, right) = in_expr 712 | l = ast.unparse(left) 713 | r = ast.unparse(right) 714 | locals_1 = locals() 715 | locals_2 = locals_1.copy() 716 | # pylint: disable=exec-used 717 | exec(f"{l} = list({r})[{idx}]", _f.f_locals | _f.f_globals, locals_2) 718 | for bind in [k for k in locals_2.keys() if k not in locals_1.keys()]: 719 | _f.f_globals[bind] = locals_2[bind] 720 | # pylint: disable=eval-used 721 | return eval(l, locals_2) 722 | 723 | def ast_match(p: Expr, expr: Any) -> bool: 724 | if isinstance(p, ast.Attribute): 725 | if expr[0] != ".": 726 | return False 727 | return ( 728 | expr[2] == p.attr 729 | and ast_match(p.value, expr[1])) 730 | elif isinstance(p, ast.Name): 731 | return p.id == expr 732 | elif isinstance(p, str): 733 | return p == expr 734 | else: 735 | raise RuntimeError(f"Can't compare: {p} == {expr}") 736 | 737 | def try_pytest_mark(p: Expr) -> Optional[Expr]: 738 | if not isinstance(p, list): 739 | return None 740 | if not len(p) == 1: 741 | return None 742 | p0 = p[0] 743 | if not isinstance(p0, ast.FunctionDef): 744 | return None 745 | if not len(p0.decorator_list) == 1: 746 | return None 747 | decorator = p0.decorator_list[0] 748 | if ast_match(decorator.func, (".", (".", "pytest", "mark"), "parametrize")): 749 | assert len(decorator.args) == 2 750 | return [ast.Name(decorator.args[0].value), decorator.args[1]] 751 | return None 752 | 753 | def to_elisp(code: str, _f: Optional[FrameType] = None) -> str: 754 | _f = _f or top_level() 755 | with io.StringIO() as buf, redirect_stdout(buf): 756 | # pylint: disable=eval-used 757 | print_elisp(eval(code, _f.f_locals | _f.f_globals)) 758 | return buf.getvalue().strip() 759 | 760 | def translate(code: str, _f: Optional[FrameType] = None, use_in_expr: bool = False) -> Any: 761 | _f = _f or sys._getframe().f_back 762 | parsed = ast.parse(code, mode="exec").body 763 | in_expr = try_in_expr(parsed) 764 | if use_in_expr and in_expr: 765 | (left, right) = in_expr 766 | out = to_elisp(ast.unparse(right), _f) 767 | nc = f"print('''{out}''')\n'select'" 768 | return ast.parse(nc).body 769 | elif has_return(parsed): 770 | r = tr_returns(parsed) 771 | assert isinstance(r, list) 772 | return wrap_return(r) 773 | else: 774 | return parsed 775 | 776 | class EvalResult(TypedDict): 777 | res: str 778 | binds: Dict[str, str] 779 | out: str 780 | err: Optional[str] 781 | 782 | def eval_code(_code: str, _env: Dict[str, Any] = {}) -> EvalResult: 783 | _res = "unset" 784 | binds = {} 785 | out = "" 786 | err: Optional[str] = None 787 | _f = _env.get("frame", sys._getframe().f_back) 788 | if "fname" in _env: 789 | _f.f_globals["__file__"] = _env["fname"] 790 | try: 791 | _code = _code or slurp(_env["code"]) 792 | new_code = translate(_code, _f, _env.get("use-in-expr", False)) 793 | (*butlast, last) = new_code 794 | _locals = {} 795 | locals_1 = _locals 796 | locals_2 = locals_1.copy() 797 | locals_globals = _f.f_locals | _f.f_globals 798 | if "debug" in _env: 799 | print(f"{ast.unparse(last)=}") 800 | with io.StringIO() as buf, redirect_stdout(buf): 801 | if butlast: 802 | # pylint: disable=exec-used 803 | exec(ast.unparse(butlast), locals_globals, locals_2) 804 | for bind in [k for k in locals_2.keys() if k not in locals_1.keys()]: 805 | _f.f_globals[bind] = locals_2[bind] 806 | try: 807 | # pylint: disable=eval-used 808 | _res = eval(ast.unparse(last), locals_globals, locals_2) 809 | except SyntaxError: 810 | locals_1 = _locals 811 | locals_2 = locals_1.copy() 812 | exec(ast.unparse(last), locals_globals, locals_2) 813 | out = buf.getvalue().strip() 814 | binds1 = [k for k in locals_2.keys() if k not in locals_1.keys()] 815 | for bind in binds1: 816 | _f.f_globals[bind] = locals_2[bind] 817 | binds2 = [bind for bind in binds1 if bind not in ["__res__", "__return__"]] 818 | print_fn = cast(Callable[..., str], to_str if _env.get("echo") else str) 819 | binds = {bind: print_fn(locals_2[bind]) for bind in binds2} 820 | # except RuntimeError as e: 821 | # if str(e) == "break": 822 | # pm() 823 | # else: 824 | # raise 825 | # pylint: disable=broad-except 826 | except Exception as e: 827 | err = f"{e.__class__.__name__}: {e}\n{e.__dict__}" 828 | _f.f_globals["e"] = e 829 | locs = e.__traceback__.tb_frame.f_locals.get("locals_2", {}) 830 | for bind in locs: 831 | _f.f_globals[bind] = locs[bind] 832 | return { 833 | "res": to_str(_res) if _env.get("echo") else repr(_res), 834 | "binds": binds, 835 | "out": out, 836 | "err": err 837 | } 838 | 839 | def eval_to_json(code: str, env: Dict[str, Any] = {}) -> None: 840 | try: 841 | env["frame"] = sys._getframe().f_back 842 | s = json.dumps(eval_code(code, env)) 843 | print(s) 844 | # pylint: disable=broad-except 845 | except Exception as e: 846 | print(json.dumps({ 847 | "res": None, 848 | "binds": {}, 849 | "out": "", 850 | "err": str(e)})) 851 | 852 | def find_module(fname: str) -> Optional[ModuleType]: 853 | for (name, module) in sys.modules.items(): 854 | if getattr(module, "__file__", None) == fname: 855 | return module 856 | return None 857 | 858 | def generate_import(code_fname: str, buffer_fname: str) -> None: 859 | code = slurp(code_fname) 860 | parsed = ast.parse(code).body[0] 861 | if isinstance(parsed, ast.FunctionDef): 862 | name = parsed.name 863 | module = find_module(buffer_fname) 864 | assert module 865 | print(f"from {module.__name__} import {name}") 866 | --------------------------------------------------------------------------------