├── .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 | [](https://travis-ci.org/abo-abo/lispy)
3 | [](https://coveralls.io/r/abo-abo/lispy?branch=master)
4 | [](http://melpa.org/#/lispy)
5 | [](http://stable.melpa.org/#/lispy)
6 |
7 |
8 |
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 | 
518 |
519 | Here's how it looks for Clojure:
520 |
521 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------