diff --git a/Cookbook.py b/Cookbook.py new file mode 100644 index 00000000..d28d54b2 --- /dev/null +++ b/Cookbook.py @@ -0,0 +1,7 @@ +def test(recipe): + return ["pytest test/test_lispy-python.py"] + +def typecheck(recipe): + return "dmypy run -- lispy-python.py" + +# del typecheck diff --git a/Makefile b/Makefile index 7a276ea6..44988dd7 100644 --- a/Makefile +++ b/Makefile @@ -30,4 +30,4 @@ clojure: clean: rm -f *.elc -.PHONY: all clean elisp check-declare +.PHONY: all clean elisp check-declare test diff --git a/le-clojure.el b/le-clojure.el index f9886c8d..bce663bd 100644 --- a/le-clojure.el +++ b/le-clojure.el @@ -68,34 +68,57 @@ "Nil if the Clojure middleware in \"lispy-clojure.clj\" wasn't loaded yet.") (defun lispy--clojure-process-buffer () - (let ((cur-type (cider-repl-type-for-buffer))) - (car (cider-repls cur-type nil)))) + (if (or org-src-mode (eq major-mode 'org-mode)) + (cadr (first (sesman--all-system-sessions 'CIDER))) + (let ((cur-type (cider-repl-type-for-buffer))) + (car (cider-repls cur-type nil))))) (defun lispy--clojure-middleware-loaded-p () (let ((conn (lispy--clojure-process-buffer))) (and conn (gethash conn lispy--clojure-middleware-loaded-hash)))) +(defun lispy--clojure-babashka-p () + (ignore-errors (cider--babashka-version))) + (defun lispy--eval-clojure-context (e-str) (cond - ((eq major-mode 'clojurescript-mode) + ((or (eq major-mode 'clojurescript-mode) + (lispy--clojure-babashka-p)) e-str) ((string-match-p "#break" e-str) e-str) ((lispy--clojure-middleware-loaded-p) - (format (if (memq this-command '(special-lispy-eval - special-lispy-eval-and-insert)) - "(lispy.clojure/pp (lispy.clojure/reval %S %S :file %S :line %S))" - "(lispy.clojure/reval %S %S :file %S :line %S)") - e-str - (condition-case nil - (let ((deactivate-mark nil)) - (save-mark-and-excursion - (lispy--out-backward 1 t) - (deactivate-mark) - (lispy--string-dwim))) - (error "")) - (buffer-file-name) - (line-number-at-pos))) + (let ((context-str + (condition-case nil + (let ((deactivate-mark nil)) + (save-mark-and-excursion + (lispy--out-backward 1 t) + (deactivate-mark) + (lispy--string-dwim))) + (error "")))) + (when (and (lispy--leftp) + (looking-back "(for[ \t\n]*" (line-beginning-position -1))) + (let* ((e-str-1 (save-excursion + (forward-char 1) + (forward-sexp 2) + (lispy--string-dwim))) + (coll (read (lispy--eval-clojure-1 + (format "(lispy.clojure/with-shadows (map str %s))" e-str-1) + nil))) + (idx (lispy--idx-from-list coll)) + (sym (save-excursion + (forward-char 1) + (lispy--string-dwim)))) + (setq e-str (format "%s (nth %s %d)" sym e-str-1 idx)))) + (format (if (memq this-command '(special-lispy-eval + special-lispy-eval-and-insert + lispy-eval-current-outline)) + "(lispy.clojure/pp (lispy.clojure/reval %S %S :file %S :line %S))" + "(lispy.clojure/reval %S %S :file %S :line %S)") + e-str + context-str + (buffer-file-name) + (line-number-at-pos)))) (t e-str))) @@ -118,12 +141,7 @@ (remove-hook 'nrepl-connected-hook 'lispy--clojure-eval-hook-lambda)) -(defvar lispy-cider-jack-in-dependencies - '(("me.raynes/fs" "1.4.6") - ("compliment" "0.3.11") - ("com.cemerick/pomegranate" "0.4.0") - ("org.tcrawley/dynapath" "0.2.5") - ("nrepl" "0.8.2"))) +(defvar lispy-cider-jack-in-dependencies nil) (defvar cider-jack-in-cljs-dependencies) (defvar cider-jack-in-dependencies) @@ -193,14 +211,10 @@ Add the standard output to the result." (or (and (stringp e-str) (lispy--eval-clojure-handle-ns e-str)) - (let* (pp - (stra (if (setq pp (string-match "\\`(lispy.clojure/\\(pp\\|reval\\)" f-str)) - f-str - f-str)) - (res (lispy--eval-nrepl-clojure stra lispy--clojure-ns)) + (let* ((res (lispy--eval-nrepl-clojure f-str lispy--clojure-ns)) (status (nrepl-dict-get res "status")) (res (cond ((or (member "namespace-not-found" status)) - (lispy--eval-nrepl-clojure stra)) + (lispy--eval-nrepl-clojure f-str)) ((member "eval-error" status) (signal 'eval-error (lispy--clojure-pretty-string (nrepl-dict-get res "err")))) @@ -212,7 +226,7 @@ Add the standard output to the result." (when out (setq lispy-eval-output (concat (propertize out 'face 'font-lock-string-face) "\n"))) - (if pp + (if (string-match "\\`(lispy.clojure/\\(pp\\|reval\\)" f-str) (condition-case nil (string-trim (read val)) (error val)) @@ -386,8 +400,6 @@ Besides functions, handles specials, keywords, maps, vectors and sets." "Mark the Clojure middleware in \"lispy-clojure.clj\" as not loaded." (puthash (lispy--clojure-process-buffer) nil lispy--clojure-middleware-loaded-hash)) -(defvar cider-jdk-src-paths) - (defun lispy-cider-load-file (filename) (let ((ns-form (cider-ns-form))) (cider-map-repls :auto @@ -400,7 +412,7 @@ Besides functions, handles specials, keywords, maps, vectors and sets." (file-name-nondirectory filename) connection))))) -(defcustom lispy-clojure-middleware-tests t +(defcustom lispy-clojure-middleware-tests nil "When non-nil, run the tests from lispy-clojure.clj when loading it." :type 'boolean :group 'lispy) @@ -424,24 +436,19 @@ Besides functions, handles specials, keywords, maps, vectors and sets." (file-attributes middleware-fname)))) (when (or (null access-time) (time-less-p access-time middleware-access-time)) (setq lispy--clojure-ns "user") - (save-window-excursion - (lispy-cider-load-file - (expand-file-name middleware-fname lispy-site-directory))) + (unless (lispy--clojure-babashka-p) + (save-window-excursion + (lispy-cider-load-file + (expand-file-name middleware-fname lispy-site-directory)))) (puthash conn middleware-access-time lispy--clojure-middleware-loaded-hash) (add-hook 'nrepl-disconnected-hook #'lispy--clojure-middleware-unload) (when (equal conn-type 'clj) - (when cider-jdk-src-paths - (let ((sources-expr - (format - "(do \n %s)" - (mapconcat - (lambda (p) (format "(cemerick.pomegranate/add-classpath %S)" p)) - cider-jdk-src-paths - "\n ")))) - (lispy--eval-clojure-cider sources-expr))) - (when lispy-clojure-middleware-tests - (lispy-message - (lispy--eval-clojure-cider "(lispy.clojure/run-lispy-tests)"))))))) + (let ((test-fname (expand-file-name "lispy-clojure-test.clj" + lispy-site-directory))) + (when (and lispy-clojure-middleware-tests + (file-exists-p test-fname)) + (lispy-message + (lispy--eval-clojure-cider (format "(load-file \"%s\")" test-fname))))))))) (defun lispy-flatten--clojure (_arg) "Inline a Clojure function at the point of its call." @@ -474,10 +481,13 @@ Besides functions, handles specials, keywords, maps, vectors and sets." (let* ((e-str (format "(lispy.clojure/debug-step-in\n'%s)" (lispy--string-dwim))) (str (substring-no-properties - (lispy--eval-clojure-cider e-str)))) + (lispy--eval-clojure-1 e-str nil))) + (old-session (sesman-current-session 'CIDER))) (lispy-follow) (when (string-match "(clojure.core/in-ns (quote \\([^)]+\\))" str) (setq lispy--clojure-ns (match-string 1 str))) + (when (equal (file-name-nondirectory (buffer-file-name)) "lispy-clojure.clj") + (sesman-link-session 'CIDER old-session)) (lispy--eval-clojure-cider str) (lispy-flow 1))) @@ -563,31 +573,7 @@ Besides functions, handles specials, keywords, maps, vectors and sets." (setq cands (all-completions (lispy--string-dwim bnd) cands))) (list (car bnd) (cdr bnd) cands))) ((eq (lispy--clojure-process-type) 'cljs) - nil) - ((save-excursion - (lispy--out-backward 2 t) - (looking-at "(import")) - (let* ((prefix (save-excursion - (lispy--out-backward 1 t) - (forward-char) - (thing-at-point 'symbol t))) - (cands (read (lispy--eval-clojure-cider - (format - "(lispy.clojure/complete %S)" - prefix)))) - (len (1+ (length prefix))) - (candsa (mapcar (lambda (s) (substring s len)) cands))) - (when (> (cdr bnd) (car bnd)) - (setq candsa (all-completions (lispy--string-dwim bnd) candsa))) - (list (car bnd) (cdr bnd) candsa))) - (t - (let* ((prefix (lispy--string-dwim bnd)) - (cands (read (lispy--eval-clojure-cider-noerror - (format - "(lispy.clojure/complete %S)" - prefix))))) - (when cands - (list (car bnd) (cdr bnd) cands)))))))))) + nil))))))) (defun lispy--eval-clojure-cider-noerror (e-str) (condition-case nil diff --git a/le-js.el b/le-js.el new file mode 100644 index 00000000..41a59843 --- /dev/null +++ b/le-js.el @@ -0,0 +1,41 @@ +(require 'indium) + +(defun lispy--eval-js (str) + (let ((r nil)) + (indium-eval + str (lambda (value) (setq r (indium-render-remote-object-to-string value)))) + (while (not r) + (accept-process-output)) + (substring-no-properties r))) + +(defun lispy--eval-js-str () + (if (region-active-p) + (lispy--string-dwim) + (lispy--string-dwim + (lispy-bounds-python-block)))) + +(defun lispy--js-completion-at-point () + (let* ((prefix (buffer-substring-no-properties + (let ((bol (point-at-bol)) + (prev-delimiter (1+ (save-excursion + (re-search-backward "[([:space:]]" nil t))))) + (if prev-delimiter + (max bol prev-delimiter) + bol)) + (point))) + (expression (if (string-match-p "\\." prefix) + (replace-regexp-in-string "\\.[^\\.]*$" "" prefix) + "this")) + (cands nil)) + (indium-client-get-completion + expression + indium-debugger-current-frame + (lambda (candidates) + (setq cands candidates))) + (while (null cands) + (accept-process-output)) + (list (- (point) (length (company-grab-symbol))) + (point) + (mapcar #'identity cands)))) + +(provide 'le-js) diff --git a/le-lisp.el b/le-lisp.el index 5aefb544..ab8061ea 100644 --- a/le-lisp.el +++ b/le-lisp.el @@ -47,16 +47,15 @@ (defun lispy--eval-lisp (str) "Eval STR as Common Lisp code." (let* ((deactivate-mark nil) - (result (with-current-buffer (process-buffer (lispy--cl-process)) - (if (lispy--use-sly-p) - (sly-eval `(slynk:eval-and-grab-output ,str)) - (slime-eval `(swank:eval-and-grab-output ,str)))))) - (if (equal (car result) "") - (cadr result) - (concat (propertize (car result) - 'face 'font-lock-string-face) - "\n\n" - (cadr result))))) + (result (if (lispy--use-sly-p) + (with-current-buffer (process-buffer (lispy--cl-process)) + (sly-eval `(slynk:eval-and-grab-output ,str))) + (slime-eval `(swank:eval-and-grab-output ,str))))) + (pcase result + (`("" "") "(ok)") + (`("" ,val) val) + (`(,out ,val) + (concat (propertize (string-trim-left out) 'face 'font-lock-string-face) "\n\n" val))))) (defun lispy--cl-process () (unless (lispy--use-sly-p) diff --git a/le-python.el b/le-python.el index 6e215754..da7aa50b 100644 --- a/le-python.el +++ b/le-python.el @@ -53,11 +53,11 @@ Stripping them will produce code that's valid for an eval." ((and (looking-at lispy-outline) (looking-at lispy-outline-header)) (lispy--bounds-outline)) - ((looking-at "@") + ((and (looking-at "@") (bolp)) (setq bnd (cons (point) (save-excursion - (forward-sexp) - (skip-chars-forward "[ \t\n]") + (re-search-forward "^def" nil t) + (goto-char (match-beginning 0)) (cdr (lispy-bounds-python-block)))))) ((setq bnd (lispy-bounds-python-block))) ((bolp) @@ -88,37 +88,45 @@ Stripping them will produce code that's valid for an eval." count)) (defun lispy-extended-eval-str (bnd) - (let ((lp (lispy--count-regex "(" bnd)) - (rp (lispy--count-regex ")" bnd))) - (save-excursion - (goto-char (cdr bnd)) - (while (< rp lp) - (re-search-forward "[()]" nil t) - (cond ((string= (match-string 0) "(") - (cl-incf lp)) - ((string= (match-string 0) ")") - (cl-incf rp)) - (t - (error "Unexpected")))) - (if (lispy-after-string-p ")") - (let ((end (point))) - (save-excursion - (forward-sexp -1) - (concat (buffer-substring-no-properties - (car bnd) (point)) - (replace-regexp-in-string - "[\\]*\n[\t ]*" " " - (replace-regexp-in-string - "^ *#.*$" "" - (buffer-substring-no-properties - (point) end)))))) - (buffer-substring-no-properties (car bnd) (point)))))) + "This function should strip python comments. +So that we can make multi-line expression into a single-line one. +It didn't work great." + (buffer-substring-no-properties (car bnd) (cdr bnd)) + ;; (let ((lp (lispy--count-regex "(" bnd)) + ;; (rp (lispy--count-regex ")" bnd))) + ;; (save-excursion + ;; (goto-char (cdr bnd)) + ;; (while (< rp lp) + ;; (re-search-forward "[()]" nil t) + ;; (cond ((string= (match-string 0) "(") + ;; (cl-incf lp)) + ;; ((string= (match-string 0) ")") + ;; (cl-incf rp)) + ;; (t + ;; (error "Unexpected")))) + ;; (if (lispy-after-string-p ")") + ;; (let ((end (point))) + ;; (save-excursion + ;; (forward-sexp -1) + ;; (concat (buffer-substring-no-properties + ;; (car bnd) (point)) + ;; (replace-regexp-in-string + ;; "[\\]*\n[\t ]*" " " + ;; (replace-regexp-in-string + ;; " *#.*[^\"]$" "" + ;; (buffer-substring-no-properties + ;; (point) end)))))) + ;; (buffer-substring-no-properties (car bnd) (point))))) + ) (defun lispy-eval-python-str (&optional bnd) (let* ((bnd (or bnd (lispy-eval-python-bnd))) (str1 (lispy-trim-python (lispy-extended-eval-str bnd))) - (str1.5 (replace-regexp-in-string "^ *#[^\n]+\n" "" str1)) + (str1.4 (if (string-match-p "\\`\\(\\w\\|\\s_\\)+ :=" str1) + (replace-regexp-in-string ":=" "=" str1) + str1)) + (str1.5 (replace-regexp-in-string "^ *#[^\n]+\n" "" str1.4)) ;; (str2 (replace-regexp-in-string "\\\\\n +" "" str1.5)) ;; (str3 (replace-regexp-in-string "\n *\\([])}]\\)" "\\1" str2)) ;; (str4 (replace-regexp-in-string "\\([({[,]\\)\n +" "\\1" str3)) @@ -168,10 +176,41 @@ Stripping them will produce code that's valid for an eval." (end-of-line 2))) (point))))) -(defvar-local lispy-python-proc nil) +(defvar-local lispy-python-buf nil) (declare-function mash-make-shell "ext:mash") +(defvar-local python-shell--interpreter nil) +(defvar-local python-shell--interpreter-args nil) + +(define-minor-mode lispy-python-interaction-mode + "Minor mode for eval-ing Python code." + :group 'lispy + (when lispy-python-interaction-mode + (when python-shell--parent-buffer + (python-util-clone-local-variables python-shell--parent-buffer)) + (setq-local indent-tabs-mode nil) + (setq-local python-shell--prompt-calculated-input-regexp nil) + (setq-local python-shell--block-prompt nil) + (setq-local python-shell--prompt-calculated-output-regexp nil) + (python-shell-prompt-set-calculated-regexps) + (setq comint-prompt-regexp python-shell--prompt-calculated-input-regexp) + (setq-local comint-prompt-read-only t) + (setq mode-line-process '(":%s")) + (setq-local comint-output-filter-functions + '(ansi-color-process-output + python-shell-comint-watch-for-first-prompt-output-filter + python-comint-postoutput-scroll-to-bottom + comint-watch-for-password-prompt)) + (setq-local compilation-error-regexp-alist python-shell-compilation-regexp-alist) + (add-hook 'completion-at-point-functions + #'python-shell-completion-at-point nil 'local) + (define-key inferior-python-mode-map "\t" + 'python-shell-completion-complete-or-indent) + (make-local-variable 'python-shell-internal-last-output) + (compilation-shell-minor-mode 1) + (python-pdbtrack-setup-tracking))) + (defun lispy-set-python-process-action (x) (when (and current-prefix-arg (consp x)) (let* ((process (cdr x)) @@ -180,20 +219,32 @@ Stripping them will produce code that's valid for an eval." (sit-for 0.01) (kill-buffer buffer) (setq x (car x)))) - (setq lispy-python-proc - (cond ((consp x) - (cdr x)) - ((require 'mash-python nil t) - (save-window-excursion - (get-buffer-process - (mash-make-shell x 'mash-new-lispy-python)))) - (t - (lispy--python-proc (concat "lispy-python-" x))))) - (unless (lispy--eval-python "lp") - (lispy-python-middleware-reload))) + (let ((buf (cond ((consp x) + (process-buffer (cdr x))) + ((require 'mash-python nil t) + (save-window-excursion + (mash-make-shell x 'mash-new-lispy-python))) + (t + (process-buffer + (lispy--python-proc (concat "lispy-python-" x))))))) + + (setq lispy-python-buf buf) + (with-current-buffer lispy-python-buf + (lispy-python-interaction-mode) + (setq lispy--python-middleware-file + (if (file-name-absolute-p lispy-python-middleware-file) + lispy-python-middleware-file + (expand-file-name "lispy-python.py" lispy-site-directory))) + (setq lispy-python-buf buf))) + (let ((lp (ignore-errors (lispy--eval-python-plain "lp")))) + (unless (and lp (string-match-p "module 'lispy-python'" lp)) + (lispy-python-middleware-reload)))) (defvar lispy-python-process-regexes - '("^lispy-python-\\(.*\\)" "\\`\\(Python\\)\\'" "\\`\\(comint\\)\\'") + '("^lispy-python-\\(.*\\)" "\\`\\(Python\\)\\'" + "\\`\\(comint.*\\)\\'" + "\\`\\(shell.*\\)\\'" + "\\`\\(gud-\\(?:pdb\\|python\\)\\)\\'") "List of regexes for process buffers that run Python.") (defun lispy-short-process-name (x) @@ -203,7 +254,10 @@ Stripping them will produce code that's valid for an eval." (mapcar (lambda (re) (when (string-match re pname) - (match-string 1 pname))) + (let ((m (match-string 1 pname))) + (if (string-match-p "comint\\|shell" m) + (buffer-name (process-buffer x)) + m)))) lispy-python-process-regexes))))) (defvar lispy-override-python-binary nil @@ -228,33 +282,68 @@ it at one time." (read-string "python binary: ")))) (ivy-read (if arg "Restart process: " "Process: ") process-names :action #'lispy-set-python-process-action - :preselect (when (process-live-p lispy-python-proc) - (lispy-short-process-name lispy-python-proc)) + :preselect (when (process-live-p (get-buffer-process lispy-python-buf)) + (lispy-short-process-name (get-buffer-process lispy-python-buf))) :caller 'lispy-set-python-process))) (defvar lispy--python-middleware-loaded-p nil "Nil if the Python middleware in \"lispy-python.py\" wasn't loaded yet.") +(defvar lispy-python-middleware-file "lispy-python.py") + +(defvar lispy--python-middleware-file "lispy-python.py") + +(defvar lispy-python-init-file (expand-file-name "~/git/site-python/init.py")) + +(defvar lispy--python-init-file nil) + +(defun lispy--python-poetry-name () + (let ((pyproject (expand-file-name "pyproject.toml" (counsel-locate-git-root)))) + (and (file-exists-p pyproject) + (not (equal python-shell-interpreter "python")) + (with-current-buffer (find-file-noselect pyproject) + (goto-char (point-min)) + (when (re-search-forward "\\[tool.poetry\\]\nname *= *\"\\([^\"]+\\)\"" nil t) + (match-string-no-properties 1)))))) + +(defun lispy--python-proc-name () + (or (and (process-live-p (get-buffer-process lispy-python-buf)) + (process-name (get-buffer-process lispy-python-buf))) + (let ((name + (or (lispy--python-poetry-name) + (if (string-match "\\(?::python \\|python_\\)\\(.*\\)\\'" python-shell-interpreter-args) + (match-string 1 python-shell-interpreter-args) + "default")))) + (concat "lispy-python-" name)))) + (defun lispy--python-proc (&optional name) (let* ((proc-name (or name - (and (process-live-p lispy-python-proc) - lispy-python-proc) - "lispy-python-default")) + (lispy--python-proc-name))) (process (get-process proc-name))) (if (process-live-p process) process (let* ((python-shell-font-lock-enable nil) (inferior-python-mode-hook nil) + (poetry-name (lispy--python-poetry-name)) (python-shell-interpreter - (cond - ((save-excursion - (goto-char (point-min)) - (looking-at "#!\\(?:/usr/bin/env \\)\\(.*\\)$")) - (match-string-no-properties 1)) - ((file-exists-p python-shell-interpreter) - (expand-file-name python-shell-interpreter)) - (t - python-shell-interpreter))) + (if poetry-name + "poetry" + python-shell-interpreter)) + (python-shell-interpreter-args + (if poetry-name + "run python" + python-shell-interpreter-args)) + ;; (python-shell-interpreter + ;; (cond + ;; ((and (file-exists-p python-shell-interpreter) + ;; (not (file-directory-p python-shell-interpreter))) + ;; (expand-file-name python-shell-interpreter)) + ;; ((save-excursion + ;; (goto-char (point-min)) + ;; (looking-at "#!\\(?:/usr/bin/env \\)\\(.*\\)$")) + ;; (match-string-no-properties 1)) + ;; (t + ;; python-shell-interpreter))) (python-binary-name (or lispy-override-python-binary (concat @@ -264,109 +353,148 @@ it at one time." " " python-shell-interpreter-args))) (buffer - (let ((python-shell-completion-native-enable nil)) + (let ((python-shell-completion-native-enable nil) + (default-directory (if poetry-name + (counsel-locate-git-root) + default-directory))) (python-shell-make-comint python-binary-name proc-name nil nil)))) + (setq lispy--python-middleware-file + (if (file-name-absolute-p lispy-python-middleware-file) + lispy-python-middleware-file + (expand-file-name "lispy-python.py" lispy-site-directory))) + (setq lispy--python-init-file lispy-python-init-file) (setq process (get-buffer-process buffer)) + (sit-for 0.1) + (unless (process-live-p process) + (pop-to-buffer buffer) + (user-error "Could not start %s" python-binary-name)) (with-current-buffer buffer (python-shell-completion-native-turn-on) - (setq lispy-python-proc process) + (setq lispy-python-buf buffer) (lispy-python-middleware-reload))) process))) (defun lispy--python-print (str) (format (if (and (memq this-command '(pspecial-lispy-eval lispy-eval)) - (null current-prefix-arg)) - "lp.pprint(%s)" - "print(repr(%s))") + (memq current-prefix-arg '(nil))) + "lp.pprint((%s))" + "print(repr((%s)))") str)) +(defun lispy--py-to-el (py) + (read (lispy--eval-python-plain (format "lp.print_elisp(%s)" py)))) + +(defun lispy--python-nth (py-lst) + (let (;; (repr (condition-case nil + ;; (read (lispy--eval-python-plain (format "print_elisp(%s)" py-expr))) + ;; (error + ;; (lispy-message "Eval-error: %s" py-expr)))) + + (len (lispy--py-to-el (format "len(list(%s))" py-lst))) + (repr (ignore-errors (lispy--py-to-el (format "list(%s)" py-lst))))) + (read + (ivy-read + "idx: " + (if repr + (cl-mapcar (lambda (x i) + (concat (number-to-string i) + " " + (cond ((listp x) + (mapconcat + (lambda (y) (if (stringp y) y (prin1-to-string y))) + x " ")) + ((stringp x) + x) + (t + (prin1-to-string x))))) + repr + (number-sequence 0 (1- len))) + (mapcar #'number-to-string (number-sequence 0 (1- len)))))))) + + (defun lispy--python-nth-element (str single-line-p) "Check if STR is of the form \"ITEM in ARRAY_LIKE\". If so, return an equivalent of ITEM = ARRAY_LIKE[IDX]; ITEM." (when (and single-line-p - (string-match "\\`\\([A-Z_a-z0-9]+\\|\\(?:([^\n]+)\\)\\) in \\(.*\\)\\'" str) + (string-match "\\`\\([A-Z_a-z0-9, ]+\\|\\(?:([^\n]+)\\)\\) in \\(.*\\)\\'" str) (not (save-excursion (beginning-of-line) (looking-at " *if")))) (let* ((vars (match-string 1 str)) (val (match-string 2 str)) - (len (read (lispy--eval-python (format "len(list(%s))" val) t))) - (repr (ignore-errors (read (lispy--eval-python (format "lp.print_elisp(%s)" val) t)))) - (idx - (read - (ivy-read - "idx: " - (if repr - (cl-mapcar (lambda (x i) - (concat (number-to-string i) - " " - (cond ((listp x) - (mapconcat - (lambda (y) (if (stringp y) y (prin1-to-string y))) - x " ")) - ((keywordp x) - (prin1-to-string x)) - (t - x)))) - repr - (number-sequence 0 (1- len))) - (mapcar #'number-to-string (number-sequence 0 (1- len)))))))) + (idx (lispy--python-nth val))) (format "%s = list (%s)[%s]\nprint ((%s))" vars val idx vars)))) (defun lispy--python-eval-string-dwim (str) (setq str (string-trim str)) - (when (string-match-p "\\`super()" str) - (let* ((cls (car (split-string (python-info-current-defun) "\\.")))) - (setq str (replace-regexp-in-string - "super()" (format "super(%s, self)" cls) str)))) (let ((single-line-p (= (cl-count ?\n str) 0))) (cond - ((string-match "^\\[" str) - (format "__last__ = %s\n%s" - str (lispy--python-print "__last__"))) - ((string-match "\\`\\(\\(?:\\sw\\|\\s_\\)+\\)\\'" str) - (lispy--python-print (match-string 1 str))) - ((and (or (string-match "\\`\\(\\(?:[., ]\\|\\sw\\|\\s_\\|[][]\\)+\\) += " str) - (string-match "\\`\\(([^)]+)\\) *=[^=]" str) - (string-match "\\`\\(\\(?:\\sw\\|\\s_\\)+ *\\[[^]]+\\]\\) *=[^=]" str)) - (save-match-data - (or single-line-p - (and (not (string-match-p "lp\\." str)) - (equal (lispy--eval-python - (format "x=lp.is_assignment(\"\"\"%s\"\"\")\nprint (x)" str) - t) - "True"))))) - (concat str "\n" (lispy--python-print (match-string 1 str)))) - ((lispy--python-nth-element str single-line-p)) - ((string-match "\\`def \\([a-zA-Z_0-9]+\\)\\s-*(\\s-*self" str) - (let ((qual-name (python-info-current-defun))) - (concat str - "\n" - (format "lp.rebind(%s, fname='%s', line=%d)" - qual-name - (buffer-file-name) - (line-number-at-pos))))) - (t - str)))) + ((string-match "\\`@pytest.mark.parametrize([^\"]+\"\\([^\"]+\\)\",\\(.*?\\)[, ]*)\\'" str) + (let* ((vars (match-string 1 str)) + (vals (match-string 2 str)) + (idx (lispy--python-nth vals))) + (format "%s = %s[%s]\nprint((%s))" vars vals idx vars))) + ((string-match-p "\"\"\"" str) + str) + ((string-match "^\\[" str) + (format "__last__ = %s\n%s" + str (lispy--python-print "__last__"))) + ((string-match "\\`\\(\\(?:\\sw\\|\\s_\\)+\\)\\'" str) + (lispy--python-print (match-string 1 str))) + ((and (or (string-match "\\`\\(\\(?:[.,* ]\\|\\sw\\|\\s_\\|[][]\\)+\\) += " str) + (string-match "\\`\\(([^)]+)\\) *=[^=]" str) + (string-match "\\`\\(\\(?:\\sw\\|\\s_\\)+ *\\[[^]]+\\]\\) *=[^=]" str) + (string-match "\\`\\(\\(?:\\sw\\|\\s_\\)+\\)\\(:[^:=]+\\) *=" str)) + (save-match-data + (or single-line-p + (and (not (string-match-p "lp\\." str)) + (equal (ignore-errors + (lispy--eval-python + (format "x=lp.is_assignment(\"\"\"%s\"\"\")\nprint (x)" str) + t)) + "True"))))) + (concat str "\n" (lispy--python-print (match-string 1 str)))) + ((lispy--python-nth-element str single-line-p)) + ((string-match "\\`def \\([a-zA-Z_0-9]+\\)\\s-*(\\s-*self" str) + (let ((qual-name (python-info-current-defun))) + (concat str + "\n" + (format "lp.rebind(%s, fname='%s', line=%d)" + qual-name + (buffer-file-name) + (line-number-at-pos))))) + ((string-match "\\`\\([^if].*\\) as \\(\\(?:\\sw\\|\\s_\\)+\\)\\'" str) + (let ((val (match-string 1 str)) + (var (match-string 2 str))) + (format "%s = %s.__enter__()" var val))) + ((eq current-prefix-arg 2) + (lispy--python-print str)) + (t + str)))) (declare-function lpy-switch-to-shell "ext:lpy") -(defun lispy--eval-python (str &optional plain) +(defun lispy--eval-python-old (str &optional plain) "Eval STR as Python code." (let ((single-line-p (= (cl-count ?\n str) 0))) (unless plain (setq str (lispy--python-eval-string-dwim str)) + ;; bind __file__ (when (string-match "__file__" str) - (lispy--eval-python (format "__file__ = '%s'\n" (buffer-file-name)) t)) + (lispy--eval-python-plain (format "__file__ = '%s'\n" (buffer-file-name)))) + ;; eliminate return (when (and single-line-p (string-match "\\`return \\(.*\\)\\'" str)) (setq str (match-string 1 str)))) (let ((res (cond ((or single-line-p (string-match "\n .*\\'" str) (string-match "\"\"\"" str)) - (python-shell-send-string-no-output - str (lispy--python-proc))) + (replace-regexp-in-string + " " "" + (python-shell-send-string-no-output + str (lispy--python-proc)))) + ;; split last line ((string-match "\\`\\([\0-\377[:nonascii:]]*\\)\n\\([^\n]*\\)\\'" str) (let* ((p1 (match-string 1 str)) (p2 (match-string 2 str)) @@ -374,65 +502,174 @@ If so, return an equivalent of ITEM = ARRAY_LIKE[IDX]; ITEM." p1 (lispy--python-proc))) p2-output) (cond - ((string-match-p "SyntaxError:\\|error:" p1-output) - (python-shell-send-string-no-output - str (lispy--python-proc))) - ((null p1-output) - (signal 'eval-error "")) - ((null (setq p2-output (lispy--eval-python p2))) - (signal 'eval-error "")) - (t - (concat - (if (string= p1-output "") - "" - (concat p1-output "\n")) - p2-output))))) + ((string-match-p "SyntaxError:\\|error:" p1-output) + (python-shell-send-string-no-output + str (lispy--python-proc))) + ((null p1-output) + (signal 'eval-error "")) + ((null (setq p2-output (lispy--eval-python p2))) + (signal 'eval-error "")) + (t + (concat + (if (string= p1-output "") + "" + (concat p1-output "\n")) + p2-output))))) (t (error "unexpected"))))) (cond - ((string-match "SyntaxError: 'return' outside function\\'" res) - (lispy--eval-python - (concat "__return__ = None\n" - (replace-regexp-in-string - "\\(^ *\\)return\\(.*\\)" - (lambda (x) - (concat - (match-string 1 x) - "__return__ =" - (if (= 0 (length (match-string 2 x))) - " None" - (match-string 2 x)))) - str) - "\n" - (lispy--python-print "__return__")) - t)) - ((string-match "^RuntimeError: break$" res) - (lpy-switch-to-shell) - (goto-char (point-max)) - (insert "lp.pm()") - (comint-send-input) - "breakpoint") - ((string-match "^Traceback.*:" res) - (set-text-properties - (match-beginning 0) - (match-end 0) - '(face error) - res) - (signal 'eval-error res)) - ((equal res "") - (setq lispy-eval-output "(ok)") - "") - ((string-match-p "^<\\(?:map\\|filter\\|generator\\|enumerate\\|zip\\) object" res) - (let ((last (car (last (split-string str "\n"))))) - (cond ((string-match "\\`lp.pprint(\\(.*\\))\\'" last) - (setq str (match-string 1 last))) - ((string-match "\\`print(repr(\\(.*\\)))\\'" last) - (setq str (match-string 1 last))))) - (lispy--eval-python (format "list(%s)" str) t)) - ((string-match-p "SyntaxError:" res) - (signal 'eval-error res)) - (t - (replace-regexp-in-string "\\\\n" "\n" res)))))) + ((string-match "SyntaxError: 'return' outside function\\'" res) + (lispy--eval-python + (concat "__return__ = None\n" + (replace-regexp-in-string + "\\(^ *\\)return\\(.*\\)" + (lambda (x) + (concat + (match-string 1 x) + "__return__ =" + (if (= 0 (length (match-string 2 x))) + " None" + (match-string 2 x)))) + str) + "\n" + (lispy--python-print "__return__")) + t)) + ((string-match "^RuntimeError: break$" res) + (lpy-switch-to-shell) + (goto-char (point-max)) + (insert "lp.pm()") + (sit-for 0.1) + (comint-send-input) + "breakpoint") + ((string-match "^Traceback.*:" res) + (set-text-properties + (match-beginning 0) + (match-end 0) + '(face error) + res) + (signal 'eval-error res)) + ((equal res "") + (setq lispy-eval-output "(ok)") + "") + ((string-match-p "^<\\(?:map\\|filter\\|generator\\|enumerate\\|zip\\) object" res) + (let ((last (car (last (split-string str "\n"))))) + (if (cond ((string-match "\\`lp.pprint(\\(.*\\))\\'" last) + (setq str (match-string 1 last))) + ((string-match "\\`print(repr(\\(.*\\)))\\'" last) + (setq str (match-string 1 last)))) + (lispy--eval-python-plain (format "%s = list(%s)\nlp.pprint(%s)" str str str)) + (lispy--eval-python-plain (format "dbg = list(%s)\nlp.pprint(dbg)" str str str))))) + ((string-match-p "SyntaxError:" res) + (signal 'eval-error res)) + (t + (replace-regexp-in-string "\\\\n" "\n" res)))))) + +(defun lispy--dict (&rest plist) + (let (k v r) + (while (setq k (pop plist)) + (setq v (pop plist)) + (push (format + "'%s': %s" + (cond ((keywordp k) + (substring (symbol-name k) 1)) + ((stringp k) + k + k) + (t + (error "Unexpected"))) + (cond ((eq v 't) + "True") + ((eq v nil) + "None") + ((stringp v) + (format "'%s'" v)) + (t + (prin1-to-string v)))) r)) + (concat "{" + (mapconcat #'identity (nreverse r) ",") + "}"))) + +(defun lispy--python-nth-1 (cands) + (let ((len (length cands))) + (read + (ivy-read + "idx: " + (cl-mapcar + (lambda (x i) + (concat + (number-to-string i) + " " + (cond ((listp x) + (mapconcat + (lambda (y) (if (stringp y) y (prin1-to-string y))) + x " ")) + ((stringp x) + x) + (t + (prin1-to-string x))))) + cands + (number-sequence 0 (1- len))))))) + +(defun lispy--eval-python-plain (str) + (python-shell-send-string-no-output + str (lispy--python-proc))) + +(defun lispy--eval-python (str &optional use-in-expr) + (setq lispy-eval-output nil) + (let* ((echo (if (eq current-prefix-arg 2) nil t)) + (fstr + (cond + ((eq current-prefix-arg 3) + (format "lp.eval_to_json(\"\"\"lp.select_item(\"%s\", 0)\"\"\")" str)) + ((or (string-match-p ".\n+." str) (string-match-p "\"\"\"" str)) + (let ((temp-file-name (python-shell--save-temp-file str))) + (format "lp.eval_to_json('', %s)" + (lispy--dict + :code temp-file-name + :fname (buffer-file-name) + :echo echo)))) + (t + (format "lp.eval_to_json(\"\"\"%s \"\"\", %s)" + (replace-regexp-in-string "\\\\n" "\\\\n" str nil t) + (lispy--dict :fname (buffer-file-name) + :echo echo + :use-in-expr use-in-expr))))) + (rs (python-shell-send-string-no-output + fstr + (lispy--python-proc))) + (extra-out (and (string-match "^{" rs) (substring rs 0 (match-beginning 0)))) + (res (json-parse-string + (substring rs (match-beginning 0)) :object-type 'plist :null-object nil)) + (val (plist-get res :res)) + (binds (plist-get res :binds)) + (out (concat extra-out (plist-get res :out))) + (err (plist-get res :err))) + (when (eq current-prefix-arg 3) + (kill-new fstr)) + (if err + (signal 'eval-error (concat out err)) + (unless (equal out "") + (setq lispy-eval-output + (concat (propertize out 'face 'font-lock-string-face) "\n"))) + (cond + ((string= val "'select'") + (setq lispy-eval-output nil) + (let* ((cands (read out)) + (idx (lispy--python-nth-1 cands))) + (lispy--eval-python-plain + (format "lp.select_item(\"\"\"%s\"\"\", %d)" str idx)))) + ((and val (not (string= val "'unset'"))) + (if (string-prefix-p "\"" val) + (read val) + val)) + (binds + (mapconcat + (lambda (x) + (concat (substring (symbol-name (car x)) 1) " = " (cadr x))) + (seq-partition binds 2) + "\n")) + (t + "(ok)"))))) (defun lispy--python-array-to-elisp (array-str) "Transform a Python string ARRAY-STR to an Elisp string array." @@ -486,7 +723,9 @@ If so, return an equivalent of ITEM = ARRAY_LIKE[IDX]; ITEM." (point))) (defun lispy-python-completion-at-point () - (cond ((looking-back "^\\(import\\|from\\) .*" (line-beginning-position)) + (cond ((save-excursion + (back-to-indentation) + (looking-at "\\(import\\|from\\) .*")) (let* ((line (buffer-substring-no-properties (line-beginning-position) (point))) @@ -494,7 +733,7 @@ If so, return an equivalent of ITEM = ARRAY_LIKE[IDX]; ITEM." (format "lp.jedi_completions('%s')" line)) (cands (lispy--python-array-to-elisp - (lispy--eval-python str))) + (lispy--eval-python-plain str))) (bnd (bounds-of-thing-at-point 'symbol)) (beg (if bnd (car bnd) (point))) (end (if bnd (cdr bnd) (point)))) @@ -506,35 +745,40 @@ If so, return an equivalent of ITEM = ARRAY_LIKE[IDX]; ITEM." (cons (point) (lispy--python-beginning-of-object)))) (str (lispy--string-dwim bnd)) - (keys (read (lispy--eval-python + (keys (read (lispy--eval-python-plain (format "lp.print_elisp(%s.keys())" str))))) (list (point) (point) (if (> (length quote_str) 0) keys - (mapcar (lambda (s) (concat "\"" s "\"")) keys))))) + (mapcar + (lambda (s) + (if (stringp s) + (concat "\"" s "\"") + (prin1-to-string s))) + keys))))) ((lispy-complete-fname-at-point)) (t (let* ((bnd (lispy-python-symbol-bnd)) (str (buffer-substring-no-properties - (car bnd) (cdr bnd)))) + (car bnd) (cdr bnd))) + (str-com str)) (when (string-match "\\`\\(.*\\)\\.[^.]*\\'" str) (let ((expr (format "__t__ = %s" (substring str 0 (match-end 1))))) - (setq str (concat "__t__" (substring str (match-end 1)))) - (cl-incf (car bnd) (match-end 1)) - (lispy--eval-python expr t))) + (setq str-com (concat "__t__" (substring str (match-end 1)))) + (cl-incf (car bnd) (1+ (- (match-end 1) (match-beginning 0)))) + (lispy--eval-python-plain expr))) (list (car bnd) (cdr bnd) - (mapcar (lambda (s) - (replace-regexp-in-string - "__t__" "" - (if (string-match "(\\'" s) - (substring s 0 (match-beginning 0)) - s))) - (python-shell-completion-get-completions - (lispy--python-proc) - nil str))))))) - -(defvar lispy--python-arg-key-re "\\`\\(\\(?:\\sw\\|\\s_\\)+\\)=\\([^=].*\\)\\'" + (let ((out (lispy--eval-python-plain + (format "lp.print_elisp(lp.get_completions('%s'))" str-com)))) + (with-temp-buffer + (insert out) + (goto-char (point-min)) + (when (re-search-forward "^(" nil t) + (goto-char (match-beginning 0))) + (read (current-buffer))))))))) + +(defvar lispy--python-arg-key-re "\\`\\(\\(?:\\sw\\|\\s_\\)+\\)=\\([^\\0]+\\)\\'" "Constant regexp for matching function keyword spec.") (defun lispy--python-args (beg end) @@ -546,8 +790,10 @@ If so, return an equivalent of ITEM = ARRAY_LIKE[IDX]; ITEM." (while (< (point) end) (forward-sexp) (while (and (< (point) end) - (not (looking-at ","))) - (forward-sexp)) + (not (looking-at ",")) + (ignore-errors + (forward-sexp) + t))) (push (buffer-substring-no-properties beg (point)) res) @@ -608,10 +854,12 @@ If so, return an equivalent of ITEM = ARRAY_LIKE[IDX]; ITEM." args)) (args-normal (cl-set-difference args args-key)) (fn-data - (read (lispy--eval-python + (read (lispy--eval-python-plain (format "lp.argspec(%s)" fn)))) (fn-args (plist-get fn-data :args)) + (fn-varkw + (plist-get fn-data :varkw)) (fn-varargs (plist-get fn-data :varargs)) (fn-keywords @@ -619,8 +867,8 @@ If so, return an equivalent of ITEM = ARRAY_LIKE[IDX]; ITEM." (fn-defaults (mapcar (lambda (x) (cond ((stringp x) - (prin1-to-string x)) - (x x) + x) + (x (prin1-to-string x)) (t "None"))) (plist-get fn-data :defaults))) @@ -650,11 +898,14 @@ If so, return an equivalent of ITEM = ARRAY_LIKE[IDX]; ITEM." (let ((arg-name (match-string 1 arg)) (arg-val (match-string 2 arg)) arg-cell) - (if (setq arg-cell (assoc arg-name fn-alist)) - (setcdr arg-cell arg-val) - (if fn-keywords - (push (concat arg-name "=" arg-val) extra-keywords) - (error "\"%s\" is not in %s" arg-name fn-alist)))) + (cond ((setq arg-cell (assoc arg-name fn-alist)) + (setcdr arg-cell arg-val)) + (fn-keywords + (push (concat arg-name "=" arg-val) extra-keywords)) + (fn-varkw + (push (cons arg-name arg-val) fn-alist)) + (t + (error "\"%s\" is not in %s" arg-name fn-alist)))) (error "\"%s\" does not match the regex spec" arg))) (when (memq nil (mapcar #'cdr fn-alist)) (error "Not all args were provided: %s" fn-alist)) @@ -669,25 +920,36 @@ If so, return an equivalent of ITEM = ARRAY_LIKE[IDX]; ITEM." (concat "dict(" (mapconcat #'identity extra-keywords ", ") ")")) fn-alist)) (setq dbg-cmd - (mapconcat (lambda (x) - (format "%s = %s" (car x) (cdr x))) - fn-alist - "; ")) + (concat + (format "lp.step_into_module_maybe(%s); " + (if method-p + (cdar fn-alist) + fn)) + "(" + (mapconcat #'car fn-alist ", ") + ")=(" + (mapconcat #'cdr fn-alist ", ") + + ")")) (condition-case nil - (lispy--eval-python dbg-cmd t) + (lispy--eval-python-plain dbg-cmd) (error (lispy--eval-python (format "lp.step_in(%s,%s)" fn (buffer-substring-no-properties (1+ p-ar-beg) (1- p-ar-end)))))) - (goto-char orig-point) - (when fn-data - (set-text-properties - 0 1 - `( - filename ,(plist-get fn-data :filename) - line ,(plist-get fn-data :line)) - fn)) - (lispy-goto-symbol fn))))) + (let ((line (plist-get fn-data :line))) + (unless (eq line 1) + (goto-char orig-point) + (when fn-data + (set-text-properties + 0 1 + `( + filename ,(plist-get fn-data :filename) + line ,line) + fn)))) + (let ((buf lispy-python-buf)) + (lispy-goto-symbol fn) + (setq lispy-python-buf buf)))))) (declare-function deferred:sync! "ext:deferred") (declare-function jedi:goto-definition "ext:jedi-core") @@ -701,6 +963,19 @@ If so, return an equivalent of ITEM = ARRAY_LIKE[IDX]; ITEM." (unless (bolp) (backward-char))) +(defun lispy--python-goto-definition () + (save-buffer) + (let ((definition (lispy--eval-python-plain + (format "lp.goto_definition('%s',%d,%d)" + (buffer-file-name) + (line-number-at-pos) + (current-column))))) + (unless (string= definition "") + (cl-destructuring-bind (fname line column) (read definition) + (lispy--goto-symbol-python fname line) + (forward-char column)) + t))) + (defun lispy-goto-symbol-python (symbol) (save-restriction (widen) @@ -708,27 +983,23 @@ If so, return an equivalent of ITEM = ARRAY_LIKE[IDX]; ITEM." (line (get-text-property 0 'line symbol))) (if file (lispy--goto-symbol-python file line) - (let ((res (ignore-errors - (or - (deferred:sync! - (jedi:goto-definition)) - t)))) - (if (member res '(nil "Definition not found.")) - (let* ((symbol (python-info-current-symbol)) - (symbol-re (concat "^\\(?:def\\|class\\).*" (car (last (split-string symbol "\\." t))))) - (r (lispy--eval-python - (format "lp.argspec(%s)" symbol))) - (plist (and r (read r)))) - (cond (plist - (lispy--goto-symbol-python - (plist-get plist :filename) - (plist-get plist :line))) - ((and (equal file "None") - (re-search-backward symbol-re nil t))) - (t - (error "Both jedi and inspect failed")))) - (unless (looking-back "def " (line-beginning-position)) - (jedi:goto-definition)))))))) + (or (lispy--python-goto-definition) + (let* ((symbol (or (python-info-current-symbol) symbol)) + (r (ignore-errors + (lispy--eval-python-plain + (format "lp.argspec(%s)" symbol)))) + (plist (and r (read r)))) + (cond (plist + (lispy--goto-symbol-python + (plist-get plist :filename) + (plist-get plist :line))) + ((and (equal file "None") + (let ((symbol-re + (concat "^\\(?:def\\|class\\).*" + (car (last (split-string symbol "\\." t)))))) + (re-search-backward symbol-re nil t)))) + (t + (error "Both jedi and inspect failed"))))))))) (defun lispy--python-docstring (symbol) "Look up the docstring for SYMBOL. @@ -737,7 +1008,7 @@ First, try to see if SYMBOL.__doc__ returns a string in the current REPL session (dynamic). Otherwise, fall back to Jedi (static)." - (let ((dynamic-result (lispy--eval-python (concat symbol ".__doc__")))) + (let ((dynamic-result (lispy--eval-python-plain (concat symbol ".__doc__")))) (if (> (length dynamic-result) 0) (mapconcat #'string-trim-left (split-string (substring dynamic-result 1 -1) "\\\\n") @@ -752,38 +1023,39 @@ Otherwise, fall back to Jedi (static)." (setq lispy--python-middleware-loaded-p nil) (lispy--python-middleware-load)) -(defvar lispy-python-init-file "~/git/site-python/init.py") +(defun lispy--python-slurp (f) + (with-temp-buffer + (insert-file-contents f) + (buffer-string))) -(defvar lispy-python-init-file-remote "/opt/lispy-python.py") +(defun lispy--python-setup-cmd () + (concat + "from importlib.machinery import SourceFileLoader;" + (format "lp=SourceFileLoader('lispy-python', '%s').load_module();" + lispy--python-middleware-file) + (format "lp.setup('%s')" lispy--python-init-file))) (defun lispy--python-middleware-load () "Load the custom Python code in \"lispy-python.py\"." (unless lispy--python-middleware-loaded-p - (let* ((lispy-python-py - (if (file-remote-p default-directory) - lispy-python-init-file-remote - (expand-file-name "lispy-python.py" lispy-site-directory))) - (module-path (format "'lispy-python','%s'" lispy-python-py))) - (lispy--eval-python - (format - (concat - "try:\n" - " from importlib.machinery import SourceFileLoader\n" - " lp=SourceFileLoader(%s).load_module()\n" - "except:\n" - " import imp;lp=imp.load_source(%s)\n" - "__name__='__repl__';" - "pm=lp.Autocall(lp.pm);") - module-path module-path)) - (when (file-exists-p lispy-python-init-file) - (lispy--eval-python - (format "exec (open ('%s').read(), globals ())" - (expand-file-name lispy-python-init-file)))) + (let* ((default-directory (or (locate-dominating-file default-directory ".git") + default-directory)) + out) + ;; send single line so that python.el does no /tmp/*.py magic, which does not work in Docker + (setq out (lispy--eval-python-plain (lispy--python-setup-cmd))) + (when (string-match "FileNotFoundError" out) + (let* ((text (lispy--python-slurp lispy--python-middleware-file)) + (ben (replace-regexp-in-string "\n" "" (base64-encode-string text))) + (setup-cmd (let ((lispy--python-middleware-file "/tmp/lispy.py")) + (lispy--python-setup-cmd)))) + (lispy--eval-python-plain + (format "import base64; open('/tmp/lispy.py','w').write(base64.b64decode('%s').decode()); %s" + ben setup-cmd)))) (setq lispy--python-middleware-loaded-p t)))) (defun lispy--python-arglist (symbol filename line column) (lispy--python-middleware-load) - (let* ((boundp (lispy--eval-python symbol)) + (let* ((boundp (ignore-errors (lispy--eval-python-plain symbol) t)) (code (if boundp (format "lp.arglist(%s)" symbol) (format "lp.arglist_jedi(%d, %d, '%s')" line column filename))) diff --git a/lispy-clojure-test.clj b/lispy-clojure-test.clj index 7be032f6..f7ce8a05 100644 --- a/lispy-clojure-test.clj +++ b/lispy-clojure-test.clj @@ -18,20 +18,20 @@ ;; see . (ns lispy-clojure-test - (:use [clojure.test :only [is deftest]] - [lispy.clojure :only [add-location-to-defn - add-location-to-def - debug-step-in - dest - expand-home - get-func-args - get-func-args-def - guess-intent - object-members - position - reader= - reval - symbol-function]])) + (:require + [clojure.test :refer [is deftest]] + [lispy.clojure :refer [add-location-to-defn + add-location-to-def + debug-step-in + dest + get-func-args + get-func-args-def + guess-intent + object-members + position + reader= + reval + symbol-function]])) (deftest get-func-args-test (is (= (get-func-args (symbol-function 'string?) 1) '[x])) @@ -76,7 +76,7 @@ (deftest debug-step-in-test (is (= (debug-step-in - '(expand-home (str "/foo" "/bar"))) + '(lispy.clojure/expand-home (str "/foo" "/bar"))) {:path "/foo/bar"})) (is (= @@ -145,7 +145,6 @@ ["/foo/bar.clj" 42])))) (deftest guess-intent-test - (is (= (guess-intent '(defproject) nil) '(lispy.clojure/fetch-packages))) (is (= (guess-intent 'x '[x y]) 'x)) (is (= (guess-intent '*ns* '*ns*) '*ns*)) (is (= (guess-intent '(+ 1 2) '[(+ 1 2) (+ 3 4) (+ 5 6)]) diff --git a/lispy-clojure.clj b/lispy-clojure.clj index d1fac077..171ac5f9 100644 --- a/lispy-clojure.clj +++ b/lispy-clojure.clj @@ -21,33 +21,22 @@ (:require [clojure.repl :as repl] [clojure.pprint] [clojure.java.io :as io] - [clojure.string :as str]) - (:use [cemerick.pomegranate :only (add-dependencies)]) - (:import (java.io File LineNumberReader InputStreamReader - PushbackReader FileInputStream) - (clojure.lang RT Reflector))) - -(defn use-package [name version] - (add-dependencies - :coordinates [[name version]] - :repositories (merge cemerick.pomegranate.aether/maven-central - {"clojars" "https://clojars.org/repo"}) - :classloader (. (. (. Compiler/LOADER deref) getParent) getParent))) - -(defn expand-file-name [name dir] - (. (io/file dir name) getCanonicalPath)) - -(use-package 'compliment "0.3.11") -(require '[compliment.core :as compliment]) - -(use-package 'me.raynes/fs "1.4.6") -(require '[me.raynes.fs :as fs]) - -(defmacro xcond [& clauses] + [clojure.string :as str] + [clojure.tools.namespace.file :as nfile] + [clojure.tools.namespace.find :as nf] + [clojure.java.classpath :as cp]) + (:import + (java.io File LineNumberReader InputStreamReader + PushbackReader FileInputStream) + (java.util.jar JarFile) + (clojure.lang RT))) + +(defmacro xcond "Common Lisp style `cond'. It's more structured than `cond', thus exprs that use it are lot more malleable to refactoring." + [& clauses] (when clauses (let [clause (first clauses)] (if (= (count clause) 1) @@ -59,23 +48,11 @@ malleable to refactoring." (xcond ~@(next clauses))))))) -(defn fetch-packages [] - (xcond ((fs/exists? "deps.edn") - (println "fixme")) - ((fs/exists? "project.clj") - (let [deps (->> (slurp "project.clj") - (read-string) - (drop 3) - (partition 2) - (map vec) - (into {}) - :dependencies)] - (doseq [[name ver] deps] - (use-package name ver)))) - (:else - (throw - (ex-info "Found no project.clj or deps.edn" - {:cwd fs/*cwd*}))))) +(defn file-exists? [f] + (. (io/file f) exists)) + +(defn expand-file-name [name dir] + (. (io/file dir name) getCanonicalPath)) (defn expand-home [path] @@ -246,7 +223,7 @@ malleable to refactoring." (let [[_do & forms] (dest bindings) [defs out] (partition-by map? forms)] `(let ~(vec (mapcat (fn [[_ n v]] [n v]) defs)) - ~@(if (not= *ns* nspc) + ~@(when (not= *ns* nspc) `((in-ns '~(ns-name nspc)))) ~@(map (fn [x] @@ -323,7 +300,7 @@ malleable to refactoring." (or (resolve sym) (first (keep #(ns-resolve % sym) (all-ns))) - (if-let [val (try (load-string (str sym)) (catch Exception e))] + (when-let [val (try (load-string (str sym)) (catch Exception _e))] (list 'variable (str val)))))] [(keyword? sym) 'keyword] @@ -352,7 +329,7 @@ malleable to refactoring." (xcond ((= 'special rsym) (->> (with-out-str - (eval (list 'clojure.repl/doc sym))) + (eval (list #'repl/doc sym))) (re-find #"\(.*\)") read-string rest (map str) @@ -413,7 +390,7 @@ malleable to refactoring." (and (reader= (first a) (first b)) (reader= (rest a) (rest b))))) - (catch Exception e + (catch Exception _e (= a b)))) (defn position [x coll equality] @@ -432,13 +409,13 @@ malleable to refactoring." expr (let [idx (position expr context reader=)] (xcond - ((#{'defproject} (first expr)) - `(fetch-packages)) ((nil? idx) expr) + ;; [x |(+ 1 2) y (+ 3 4)] => {:x 3} - ;; TODO: would be better to have 1 level higher context, so that we just check - ;; (= (first context) 'let) + ((and (= (first context) 'let) (= idx 1)) + (shadow-dest expr)) + ((and (vector? context) (= 0 (rem (count context) 2)) (= 0 (rem (inc idx) 2)) @@ -468,9 +445,8 @@ malleable to refactoring." (= 'defn (first expr)) file line) (let [arglist-pos (first (keep-indexed - (fn [i x] (if (or - (vector? x) - (list? x)) i)) + (fn [i x] (when (or (vector? x) (list? x)) + i)) expr)) expr-head (take arglist-pos expr) expr-tail (drop arglist-pos expr) @@ -509,7 +485,7 @@ malleable to refactoring." (java.io.StringReader. s))] (loop [res []] (if-let [x (try (read reader) - (catch Exception e))] + (catch Exception _e))] (recur (conj res x)) res)))) @@ -534,14 +510,51 @@ malleable to refactoring." (clojure.core/str "error: " ~ 'e))))))) (defn file->elisp [f] - (if (fs/exists? f) + (if (file-exists? f) f (. (io/resource f) getPath))) +(defonce ns-to-jar (atom {})) + +(defn ns-location [sym] + (when (empty? @ns-to-jar) + (reset! ns-to-jar + (apply hash-map + (->> + (cp/classpath) + (mapcat #(interleave + (if (. % isFile) + (nf/find-namespaces-in-jarfile (JarFile. %)) + (nf/find-namespaces-in-dir % nil)) + (repeat %))))))) + (let [dir (get @ns-to-jar sym)] + (if (. dir isFile) + (let [jf (JarFile. dir) + file-in-jar (first + (filter + (fn [f] + (let [entry (nf/read-ns-decl-from-jarfile-entry jf f nil)] + (when (and entry (= (first entry) sym)) + f))) + (nf/clojure-sources-in-jar jf)))] + (list + (str "file:" dir "!/" file-in-jar) + 0)) + (let [file-in-dir (first + (filter + (fn [f] + (let [decl (nfile/read-file-ns-decl f nil)] + (and decl (= (first decl) sym) + f))) + (nf/find-clojure-sources-in-dir dir)))] + (list (.getCanonicalPath file-in-dir) 0))))) + (defn location [sym] (let [rs (resolve sym) m (meta rs)] - (xcond ((:l-file m) + (xcond + ((and (nil? rs) (ns-location sym))) + ((:l-file m) (list (:l-file m) (:l-line m))) ((and (:file m) (not (re-matches #"^/tmp/" (:file m)))) (list (file->elisp (:file m)) (:line m)))))) @@ -562,14 +575,3 @@ malleable to refactoring." (fn [v] (let [m (meta v)] (str v "\n" (:arglists m) "\n" (:doc m)))))))) - -(defn complete [prefix] - (compliment/completions - prefix - {:context :same :plain-candidates true})) - -(defn run-lispy-tests [] - (let [dd (fs/parent (:file (meta #'use-package))) - fname (java.io.File. dd "lispy-clojure-test.clj")] - (when (fs/exists? fname) - (load-file (str fname))))) diff --git a/lispy-inline.el b/lispy-inline.el index 7b8c5900..7b019234 100644 --- a/lispy-inline.el +++ b/lispy-inline.el @@ -218,23 +218,15 @@ The caller of `lispy--show' might use a substitute e.g. `describe-function'." (declare-function geiser-doc-symbol-at-point "geiser-doc") -(defun lispy--describe-inline () +(defun lispy--describe-inline (str pos) "Toggle the overlay hint." (condition-case nil - (let ((new-hint-pos (lispy--hint-pos)) - doc) - (if (and (eq lispy-hint-pos new-hint-pos) - (overlayp lispy-overlay)) - (lispy--cleanup-overlay) - (save-excursion - (when (= 0 (count-lines (window-start) (point))) - (recenter 1)) - (setq lispy-hint-pos new-hint-pos) - (if (eq major-mode 'scheme-mode) - (geiser-doc-symbol-at-point) - (when (setq doc (lispy--docstring (lispy--current-function))) - (goto-char lispy-hint-pos) - (lispy--show (propertize doc 'face 'lispy-face-hint))))))) + (save-excursion + (when (= 0 (count-lines (window-start) (point))) + (recenter 1)) + (setq lispy-hint-pos pos) + (goto-char lispy-hint-pos) + (lispy--show (propertize str 'face 'lispy-face-hint))) (error (lispy--cleanup-overlay)))) @@ -310,13 +302,22 @@ The caller of `lispy--show' might use a substitute e.g. `describe-function'." (defun lispy-describe-inline () "Display documentation for `lispy--current-function' inline." (interactive) - (if (cl-some - (lambda (window) - (equal (buffer-name (window-buffer window)) "*lispy-help*")) - (window-list)) - (when (window-configuration-p lispy--di-window-config) - (set-window-configuration lispy--di-window-config)) - (lispy--describe-inline))) + (cond ((cl-some + (lambda (window) + (equal (buffer-name (window-buffer window)) "*lispy-help*")) + (window-list)) + (when (window-configuration-p lispy--di-window-config) + (set-window-configuration lispy--di-window-config))) + ((eq major-mode 'scheme-mode) + (geiser-doc-symbol-at-point)) + (t + (let ((new-hint-pos (lispy--hint-pos))) + (if (and (eq lispy-hint-pos new-hint-pos) + (overlayp lispy-overlay)) + (lispy--cleanup-overlay) + (lispy--describe-inline + (lispy--docstring (lispy--current-function)) + new-hint-pos)))))) (declare-function lispy--python-docstring "le-python") (declare-function lispy--python-arglist "le-python") diff --git a/lispy-occur.el b/lispy-occur.el new file mode 100644 index 00000000..113f6960 --- /dev/null +++ b/lispy-occur.el @@ -0,0 +1,216 @@ +;;; lispy-occur.el --- Select a line within the current top level sexp. -*- lexical-binding: t -*- + +;; Copyright (C) 2014-2021 Oleh Krehel + +;; This file is not part of GNU Emacs + +;; This file is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation; either version 3, or (at your option) +;; any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; For a full copy of the GNU General Public License +;; see . + +;;; Commentary: +;; + +;;; Code: +(require 'swiper) + +(defcustom lispy-occur-backend 'ivy + "Method to navigate to a line with `lispy-occur'." + :type '(choice + (const :tag "Ivy" ivy) + (const :tag "Helm" helm))) + +(defvar lispy--occur-beg 1 + "Start position of the top level sexp during `lispy-occur'.") + +(defvar lispy--occur-end 1 + "End position of the top level sexp during `lispy-occur'.") + +(defun lispy--occur-candidates (&optional bnd) + "Return the candidates for `lispy-occur'." + (setq bnd (or bnd (save-excursion + (unless (and (bolp) + (lispy-left-p)) + (beginning-of-defun)) + (lispy--bounds-dwim)))) + (let ((line-number -1) + candidates) + (setq lispy--occur-beg (car bnd)) + (setq lispy--occur-end (cdr bnd)) + (save-excursion + (goto-char lispy--occur-beg) + (while (< (point) lispy--occur-end) + (push (format "%-3d %s" + (cl-incf line-number) + (buffer-substring + (line-beginning-position) + (line-end-position))) + candidates) + (forward-line 1))) + (nreverse candidates))) + +(defun lispy--occur-preselect () + "Initial candidate regex for `lispy-occur'." + (format "^%d" + (- + (line-number-at-pos (point)) + (line-number-at-pos lispy--occur-beg)))) + +(defvar helm-input) +(declare-function helm "ext:helm") + +(defun lispy-occur-action-goto-paren (x) + "Goto line X for `lispy-occur'." + (setq x (read x)) + (goto-char lispy--occur-beg) + (let ((input (if (eq lispy-occur-backend 'helm) + helm-input + ivy-text)) + str-or-comment) + (cond ((string= input "") + (forward-line x) + (back-to-indentation) + (when (re-search-forward lispy-left (line-end-position) t) + (goto-char (match-beginning 0)))) + + ((setq str-or-comment + (progn + (forward-line x) + (re-search-forward (ivy--regex input) + (line-end-position) t) + (lispy--in-string-or-comment-p))) + (goto-char str-or-comment)) + + ((re-search-backward lispy-left (line-beginning-position) t) + (goto-char (match-beginning 0))) + + ((re-search-forward lispy-left (line-end-position) t) + (goto-char (match-beginning 0))) + + (t + (back-to-indentation))))) + +(defun lispy-occur-action-goto-end (x) + "Goto line X for `lispy-occur'." + (setq x (read x)) + (goto-char lispy--occur-beg) + (forward-line x) + (re-search-forward (ivy--regex ivy-text) (line-end-position) t)) + +(defun lispy-occur-action-goto-beg (x) + "Goto line X for `lispy-occur'." + (when (lispy-occur-action-goto-end x) + (goto-char (match-beginning 0)))) + +(defun lispy-occur-action-mc (_x) + "Make a fake cursor for each `lispy-occur' candidate." + (let ((cands (nreverse ivy--old-cands)) + cand) + (while (setq cand (pop cands)) + (goto-char lispy--occur-beg) + (forward-line (read cand)) + (re-search-forward (ivy--regex ivy-text) (line-end-position) t) + (when cands + (mc/create-fake-cursor-at-point)))) + (multiple-cursors-mode 1)) + +(ivy-set-actions + 'lispy-occur + '(("m" lispy-occur-action-mc "multiple-cursors") + ("j" lispy-occur-action-goto-beg "goto start") + ("k" lispy-occur-action-goto-end "goto end"))) + +(defvar ivy-last) +(declare-function ivy-state-window "ext:ivy") + +;;;###autoload +(defun lispy-occur () + "Select a line within current top level sexp. +See `lispy-occur-backend' for the selection back end." + (interactive) + (swiper--init) + (cond ((eq lispy-occur-backend 'helm) + (require 'helm) + (add-hook 'helm-move-selection-after-hook + #'lispy--occur-update-input-helm) + (add-hook 'helm-update-hook + #'lispy--occur-update-input-helm) + (unwind-protect + (helm :sources + `((name . "this defun") + (candidates . ,(lispy--occur-candidates)) + (action . lispy-occur-action-goto-paren) + (match-strict . + (lambda (x) + (ignore-errors + (string-match + (ivy--regex helm-input) x))))) + :preselect (lispy--occur-preselect) + :buffer "*lispy-occur*") + (swiper--cleanup) + (remove-hook 'helm-move-selection-after-hook + #'lispy--occur-update-input-helm) + (remove-hook 'helm-update-hook + #'lispy--occur-update-input-helm))) + ((eq lispy-occur-backend 'ivy) + (unwind-protect + (ivy-read "pattern: " + (lispy--occur-candidates) + :preselect (lispy--occur-preselect) + :require-match t + :update-fn (lambda () + (lispy--occur-update-input + ivy-text + (ivy-state-current ivy-last))) + :action #'lispy-occur-action-goto-paren + :caller 'lispy-occur) + (swiper--cleanup) + (when (null ivy-exit) + (goto-char swiper--opoint)))) + (t + (error "Bad `lispy-occur-backend': %S" lispy-occur-backend)))) + +(defun lispy--occur-update-input-helm () + "Update selection for `lispy-occur' using `helm' back end." + (lispy--occur-update-input + helm-input + (buffer-substring-no-properties + (line-beginning-position) + (line-end-position)))) + +(defun lispy--occur-update-input (input str) + "Update selection for `ivy-occur'. +INPUT is the current input text. +STR is the full current candidate." + (swiper--cleanup) + (let ((re (ivy--regex input)) + (num (if (string-match "^[0-9]+" str) + (string-to-number (match-string 0 str)) + 0))) + (with-selected-window (ivy-state-window ivy-last) + (goto-char lispy--occur-beg) + (when (cl-plusp num) + (forward-line num) + (unless (<= (point) lispy--occur-end) + (recenter))) + (let ((ov (make-overlay (line-beginning-position) + (1+ (line-end-position))))) + (overlay-put ov 'face 'swiper-line-face) + (overlay-put ov 'window (ivy-state-window ivy-last)) + (push ov swiper--overlays)) + (re-search-forward re (line-end-position) t) + (swiper--add-overlays + re + lispy--occur-beg + lispy--occur-end)))) + +;;; lispy-occur.el ends here diff --git a/lispy-pkg.el b/lispy-pkg.el index 86fd72f4..9794ae70 100644 --- a/lispy-pkg.el +++ b/lispy-pkg.el @@ -3,6 +3,6 @@ '((emacs "24.3") (ace-window "0.9.0") (iedit "0.9.9") - (counsel "0.11.0") + (swiper "0.13.4") (hydra "0.14.0") - (zoutline "0.1.0"))) + (zoutline "0.2.0"))) diff --git a/lispy-python.py b/lispy-python.py index fe88ae7f..f39aef6c 100644 --- a/lispy-python.py +++ b/lispy-python.py @@ -18,33 +18,58 @@ # see . #* Imports -from __future__ import print_function import ast -import sys +import collections +import importlib import inspect +import io +import json +import os +import pprint as pp import re -import platform import shlex -import types -import collections -import pprint as pp +import subprocess +import sys +from ast import AST +from contextlib import redirect_stdout +from typing import List, Dict, Any, Union, Tuple, Optional, TypedDict, Callable, cast +from types import TracebackType, MethodType, FunctionType, ModuleType, FrameType + +def sh(cmd: str) -> str: + r = subprocess.run( + shlex.split(cmd), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + check=True) + return r.stdout.strip() + try: import reprlib repr1 = reprlib.Repr() repr1.maxlist = 10 - repr1.maxstring = 100 + repr1.maxstring = 200 except: pass try: import jedi except: - print("failed to load jedi") + try: + pyenv_version = sh("pyenv global") + pyversion = ".".join(pyenv_version.split(".")[:-1]) + site_packages = os.path.expanduser(f"~/.pyenv/versions/{pyenv_version}/lib/python{pyversion}/site-packages/") + sys.path.append(site_packages) + import jedi + except: + print("Failed to load jedi. Some features won't work") #* Classes class Stack: - line_numbers = {} - def __init__(self, tb): + line_numbers: Dict[Tuple[str, str], int] = {} + + def __init__(self, tb: Optional[TracebackType]): self.stack = [] + self.stack_idx = 0 while tb: name = tb.tb_frame.f_code.co_name fname = tb.tb_frame.f_code.co_filename @@ -55,15 +80,16 @@ def __init__(self, tb): self.stack.append((fname, lineno, tb.tb_frame)) tb = tb.tb_next self.stack_top = len(self.stack) - 1 - self.set_frame(self.stack_top) + if self.stack_top >= 0: + self.set_frame(self.stack_top) - def frame_string(self, i): + def frame_string(self, i: int) -> str: (fname, line, f) = self.stack[i] res = " File \"%s\", line %d, Frame [%d/%d] (%s):" % ( f.f_code.co_filename, line, i, self.stack_top, f.f_code.co_name) return res - def __repr__(self): + def __repr__(self) -> str: frames = [] for i in range(self.stack_top + 1): s = self.frame_string(i) @@ -72,124 +98,111 @@ def __repr__(self): frames.append(s) return "\n".join(frames) - def set_frame(self, i): - f = self.stack[i][2] - self.stack_idx = i - tf = top_level() - tf.f_globals["lnames"] = f.f_locals.keys() - for (k, v) in f.f_locals.items(): - tf.f_globals[k] = v - for (k, v) in f.f_globals.items(): - tf.f_globals[k] = v + def set_frame(self, i: int) -> None: + if i >= 0: + f = self.stack[i][2] + self.stack_idx = i + tf = top_level() + tf.f_globals["lnames"] = f.f_locals.keys() + for (k, v) in f.f_locals.items(): + tf.f_globals[k] = v + for (k, v) in f.f_globals.items(): + tf.f_globals[k] = v - print(self.frame_string(self.stack_idx)) + print(self.frame_string(self.stack_idx)) - def up(self, delta = 1): + def up(self, delta: int = 1) -> None: if self.stack_idx <= 0: - print("top frame already") - print(self.frame_string(self.stack_idx)) + if self.stack: + print(self.frame_string(self.stack_idx)) else: self.stack_idx = max(self.stack_idx - delta, 0) self.set_frame(self.stack_idx) - def down(self, delta = 1): + def down(self, delta: int = 1) -> None: if self.stack_idx >= self.stack_top: - print("bottom frame already") - print(self.frame_string(self.stack_idx)) + if self.stack: + print(self.frame_string(self.stack_idx)) else: self.stack_idx = min(self.stack_idx + delta, self.stack_top) self.set_frame(self.stack_idx) class Autocall: - def __init__(self, f): + def __init__(self, f: Callable): self.f = f - def __call__(self, n): + def __call__(self, n: Any) -> None: self.f(n) - def __repr__(self): - self.f() + def __repr__(self) -> str: + try: + self.f() + except: + pass return "" #* Functions -def arglist_retrieve_java(method): - name = method.__name__ - if hasattr(method, "argslist"): - # uses only the first args list... - args = [x.__name__ for x in method.argslist[0].args] - else: - methods = eval("method.__self__.class.getDeclaredMethods()") - methods_by_name = [m for m in methods if m.getName() == name] - assert len(methods_by_name) == 1, "expected only a single method by name %s" % name - meta = methods_by_name[0] - args = [str(par.getType().__name__ + " " + par.getName()) for par in meta.getParameters()] - return inspect.ArgSpec(args, None, None, None) - -def arglist_retrieve(sym): +def get_import_name(fname: str) -> str: + for p in sys.path: + if p == "": + continue + if fname.startswith(p): + return fname[len(p) + 1:].partition(".")[0].replace("/", ".") + return os.path.splitext(os.path.basename(fname))[0] + +def chfile(f: str) -> None: + tf = top_level() + tf.f_globals["__file__"] = f + name = get_import_name(f) + tf.f_globals["__name__"] = name + d = os.path.dirname(f) try: - if hasattr(inspect, "getfullargspec"): - res = inspect.getfullargspec(sym) - return inspect.ArgSpec(args = res.args, - varargs = res.varargs, - defaults = res.defaults, - keywords = res.kwonlydefaults) - else: - return inspect.getargspec(sym) - except TypeError as er: - if(re.search("is not a Python function$", er.message) - and platform.system() == "Java"): - if inspect.isclass(sym): - return arglist_retrieve(sym.__init__) - elif hasattr(sym, "argslist") or \ - hasattr(sym, "__self__") and hasattr(sym.__self__, "class"): - return arglist_retrieve_java(sym) - else: - print(er.message) - else: - print(er.message) - -def format_arg(arg_pair): - name, default_value = arg_pair - if default_value: - return name + " = " + default_value - else: - return name - -def delete(element, lst): - return [x for x in lst if x != element] - -def mapcar(func, lst): - """Compatibility function for Python3. - - In Python2 `map' returns a list, as expected. But in Python3 - `map' returns a map object that can be converted to a list. - """ - return list(map(func, lst)) + os.chdir(d) + if "sys" not in tf.f_globals: + tf.f_globals["sys"] = importlib.import_module("sys") + if name not in tf.f_globals["sys"].modules: + try: + mod = importlib.import_module(name) + tf.f_globals["sys"].modules[name] = mod + except: + pass + except: + raise -def arglist(sym): - arg_info = arglist_retrieve(sym) +def arglist(sym: Callable) -> List[str]: + def format_arg(arg_pair: Tuple[str, Optional[str]]) -> str: + name, default_value = arg_pair + if default_value: + return name + " = " + default_value + else: + return name + arg_info = inspect.getfullargspec(sym) if "self" in arg_info.args: arg_info.args.remove("self") if arg_info.defaults: - defaults = [None] *(len(arg_info.args) - len(arg_info.defaults)) + \ - mapcar(repr, arg_info.defaults) - args = mapcar(format_arg, zip(arg_info.args, defaults)) + defaults: List[Optional[str]] = [None] * (len(arg_info.args) - len(arg_info.defaults)) + defaults += [repr(x) for x in arg_info.defaults] + args = [format_arg(x) for x in zip(arg_info.args, defaults)] else: args = arg_info.args if arg_info.varargs: args += arg_info.varargs - if arg_info.keywords: - if type(arg_info.keywords) is dict: - for k, v in arg_info.keywords.items(): - args.append("%s = %s" %(k, v)) - else: - args.append("**" + arg_info.keywords) + keywords = arg_info.kwonlydefaults + if keywords: + for k, v in keywords.items(): + args.append(f"{k} = {v}") return args -def print_elisp(obj, end="\n"): +def print_elisp(obj: Any, end: str = "\n") -> None: if hasattr(obj, "_asdict") and obj._asdict is not None: + if hasattr(type(obj), "__repr__"): + print('"' + str(obj).replace('"', '') + '"') + return # namedtuple - print_elisp(obj._asdict(), end) + try: + print_elisp(obj._asdict(), end) + except: + print('"' + str(obj) + '"') elif hasattr(obj, "__array__"): # something that converts to a numpy array print_elisp(list(obj.__array__())) @@ -216,60 +229,75 @@ def print_elisp(obj, end="\n"): for (k, v) in obj: print_elisp((k, v), end="\n") print(")") - elif isinstance(obj, collections.KeysView): + elif isinstance(obj, collections.abc.KeysView): print_elisp(list(obj)) elif isinstance(obj, int): print(obj) + elif isinstance(obj, list) or isinstance(obj, tuple): + print("(", end="") + for x in obj: + print_elisp(x) + print(")") else: - if obj: - if type(obj) is list or type(obj) is tuple: - print("(", end="") - for x in obj: - print_elisp(x) - print(")") - elif type(obj) is str: + if obj is not None: + if type(obj) is str: # quote strings? # print("\"'" + re.sub("\"", "\\\"", obj) + "'\"", end=" ") print('"' + re.sub("\"", "\\\"", obj) + '"', end=" ") + elif hasattr(obj, "to_dict"): + print_elisp(obj.to_dict()) else: - print('"' + repr(obj) + '"', end=" ") + print('"' + repr(obj) + '"', end=" ") else: print('nil', end=end) def argspec(sym): - arg_info = inspect.getargspec(sym) + arg_info = inspect.getfullargspec(sym) if arg_info: di = arg_info._asdict() fn = sym.__init__ if type(sym) is type else sym - filename = fn.__code__.co_filename - di["filename"] = filename - if hasattr(sym, "__self__"): - # bound method - qname = sym.__self__.__class__.__name__ + "." + sym.__name__ - else: - qname = sym.__qualname__ - tu = (filename, qname) - if tu in Stack.line_numbers: - di["line"] = Stack.line_numbers[tu] - else: - di["line"] = fn.__code__.co_firstlineno + try: + filename = fn.__code__.co_filename + di["filename"] = filename + if hasattr(sym, "__self__"): + # bound method + qname = sym.__self__.__class__.__name__ + "." + sym.__name__ + else: + qname = sym.__qualname__ + tu = (filename, qname) + if tu in Stack.line_numbers: + di["line"] = Stack.line_numbers[tu] + else: + di["line"] = fn.__code__.co_firstlineno + except AttributeError: + m = sys.modules[sym.__module__] + filename = m.__file__ + nodes = ast.parse(open(filename).read()).body + for node in nodes: + if (type(node) in [ast.ClassDef, ast.FunctionDef] and + node.name == sym.__name__): + di["filename"] = filename + di["line"] = node.lineno + print_elisp(di) else: print("nil") def arglist_jedi(line, column, filename): - script = jedi.Script(None, line, column, filename) - defs = script.goto_definitions() - if len(defs) == 0: - raise TypeError("0 definitions found") - elif len(defs) > 1: - raise TypeError(">1 definitions found") + script = jedi.Script(path=filename) + defs = script.get_signatures(line, column) + if defs: + return [x.name for x in defs[0].params] else: - return delete('', mapcar(lambda x: str(x.name), defs[0].params)) + return [] def jedi_completions(line): - script=jedi.Script(source=line, line=1, column=len(line), sys_path=sys.path) - return [_x_.name for _x_ in script.completions()] + script = jedi.Script(code=line) + return [x.name for x in script.complete()] + +def jedi_file_completions(fname, line, column): + script = jedi.Script(path=fname) + return [x.name for x in script.complete(line, column)] def is_assignment(code): ops = ast.parse(code).body @@ -280,13 +308,15 @@ def top_level(): f = sys._getframe() while f.f_back: f = f.f_back + if f.f_code.co_filename == "": + return f return f def list_step(varname, lst): f_globals = top_level().f_globals try: val = f_globals[varname] - i =(lst.index(val) + 1) % len(lst) + i = (lst.index(val) + 1) % len(lst) except: i = 0 val = lst[i] @@ -294,7 +324,7 @@ def list_step(varname, lst): f_globals[varname] = val return val -def argv(cmd): +def argv(cmd: str) -> None: sys.argv = shlex.split(cmd) def find_global_vars(class_name): @@ -312,11 +342,11 @@ def rebind(method, fname=None, line=None): (cls_name, fun_name) = qname.split(".") for (n, v) in find_global_vars(cls_name): print("rebind:", n) - top_level().f_globals[n].__dict__[fun_name] = types.MethodType(top_level().f_globals[fun_name], v) + top_level().f_globals[n].__dict__[fun_name] = MethodType(top_level().f_globals[fun_name], v) if fname and line: Stack.line_numbers[(fname, qname)] = line -def pm(): +def pm() -> None: """Post mortem: recover the locals and globals from the last traceback.""" if hasattr(sys, 'last_traceback'): stack = Stack(sys.last_traceback) @@ -327,9 +357,7 @@ def pm(): tl.f_globals["dn"] = Autocall(stack.down) globals()["stack"] = stack -pp1 = pp.PrettyPrinter(width=100) - -def pprint(x): +def pprint(x: Any) -> None: r1 = repr(x) if len(r1) > 1000 and repr1: print(repr1.repr(x)) @@ -337,10 +365,501 @@ def pprint(x): if type(x) == collections.OrderedDict: print("{" + ",\n ".join([str(k) + ": " + str(v) for (k, v) in x.items()]) + "}") else: - pp1.pprint(x) + pp.PrettyPrinter(width=200).pprint(x) + +def to_str(x: Any) -> str: + with io.StringIO() as buf, redirect_stdout(buf): + pprint(x) + return buf.getvalue().strip() def step_in(fn, *args): - spec = inspect.getargspec(fn) + spec = inspect.getfullargspec(fn) f_globals = top_level().f_globals for (arg_name, arg_val) in zip(spec.args, args): f_globals[arg_name] = arg_val + +def step_into_module_maybe(module): + if isinstance(module, FunctionType): + try: + module = sys.modules[module.__module__] + except: + pass + elif getattr(module, "__module__", None): + if module.__module__ == "__repl__": + return + module = sys.modules[module.__module__] + if inspect.ismodule(module): + tf = top_level() + for (k, v) in module.__dict__.items(): + if not re.match("^__", k): + print(k) + tf.f_globals[k] = v + +def slurp(fname: str) -> str: + """Return `fname' contents as text.""" + with open(fname, "r", encoding="utf-8") as fh: + return fh.read() + +def definitions(path): + (_, ext) = os.path.splitext(path) + if ext == ".yml": + return yaml_definitions(path) + script = jedi.Script(slurp(path), path=path) + res = [] + for x in script.get_names(): + if (x.get_definition_start_position()[0] == x.get_definition_end_position()[0] + and "import" in x.get_line_code()): + continue + if x.type == "function": + try: + desc = x.description + "(" + ", ".join(p.name for p in x.params) + ")" + except: + desc = x.description + res.append([desc, x.line]) + elif x.type == "module": + res.append(["import " + x.name, x.line]) + elif x.type == "class": + res.append([x.description, x.line]) + try: + members = x.defined_names() + except: + members = [] + for m in members: + res.append([x.name + "." + m.name, m.line]) + else: + res.append([x.description, x.line]) + return res + + +def yaml_definitions(path): + res = [] + ls = slurp(path).strip().splitlines() + prev = "" + symbol = "(\\w|[-_])+" + for (i, line) in enumerate(ls, 1): + if m := re.match(f"^({symbol})", line): + res.append([m.group(1), i]) + prev = m.group(1) + "." + elif m := re.match(f"^ ({symbol})", line): + res.append([prev + m.group(1), i]) + return res + + +def get_completions_readline(text): + completions = [] + completer = None + try: + import readline + # pylint: disable=unused-import + import rlcompleter + completer = readline.get_completer() + if getattr(completer, 'PYTHON_EL_WRAPPED', False): + completer.print_mode = False + i = 0 + while True: + completion = completer(text, i) + if not completion: + break + i += 1 + if not re.match("[0-9]__", completion): + completions.append(completion) + except: + pass + finally: + if getattr(completer, 'PYTHON_EL_WRAPPED', False): + completer.print_mode = True + return [re.sub("__t__.", "", c) for c in completions] + +def get_completions(text): + completions = get_completions_readline(text) + if completions: + return sorted(completions) + m = re.match(r"([^.]+)\.(.*)", text) + if m: + (obj, part) = m.groups() + regex = re.compile("^" + part) + o = top_level().f_globals[obj] + items = list(o.__dict__.keys()) if hasattr(o, "__dict__") else [] + items += list(type(o).__dict__.keys()) if hasattr(type(o), "__dict__") else [] + for x in set(items): + if re.match(regex, x): + if not x.startswith("_") or part.startswith("_"): + completions.append(x) + return sorted(completions) + else: + return [] + +def __PYTHON_EL_native_completion_setup(): + import readline + try: + import __builtin__ + except ImportError: + # Python 3 + import builtins as __builtin__ + + builtins = dir(__builtin__) + is_ipython = ('__IPYTHON__' in builtins or + '__IPYTHON__active' in builtins) + + class __PYTHON_EL_Completer: + '''Completer wrapper that prints candidates to stdout. + + It wraps an existing completer function and changes its behavior so + that the user input is unchanged and real candidates are printed to + stdout. + + Returned candidates are '0__dummy_completion__' and + '1__dummy_completion__' in that order ('0__dummy_completion__' is + returned repeatedly until all possible candidates are consumed). + + The real candidates are printed to stdout so that they can be + easily retrieved through comint output redirect trickery. + ''' + + PYTHON_EL_WRAPPED = True + + def __init__(self, completer): + self.completer = completer + self.last_completion = None + self.print_mode = True + + def __call__(self, text, state): + if state == 0: + # Set the first dummy completion. + self.last_completion = None + completion = '0__dummy_completion__' + else: + completion = self.completer(text, state - 1) + + if not completion: + if self.last_completion != '1__dummy_completion__': + # When no more completions are available, returning a + # dummy with non-sharing prefix allow ensuring output + # while preventing changes to current input. + # Coincidentally it's also the end of output. + completion = '1__dummy_completion__' + elif completion.endswith('('): + # Remove parens on callables as it breaks completion on + # arguments (e.g. str(Ari)). + completion = completion[:-1] + self.last_completion = completion + + if completion in ( + '0__dummy_completion__', '1__dummy_completion__'): + return completion + elif completion: + # For every non-dummy completion, return a repeated dummy + # one and print the real candidate so it can be retrieved + # by comint output filters. + if self.print_mode: + print(completion) + return '0__dummy_completion__' + else: + return completion + else: + return completion + + completer = readline.get_completer() + + if not completer: + # Used as last resort to avoid breaking customizations. + # pylint: disable=unused-import + import rlcompleter + completer = readline.get_completer() + + if completer and not getattr(completer, 'PYTHON_EL_WRAPPED', False): + # Wrap the existing completer function only once. + new_completer = __PYTHON_EL_Completer(completer) + if not is_ipython: + readline.set_completer(new_completer) + else: + # Try both initializations to cope with all IPython versions. + # This works fine for IPython 3.x but not for earlier: + readline.set_completer(new_completer) + # IPython<3 hacks readline such that `readline.set_completer` + # won't work. This workaround injects the new completer + # function into the existing instance directly: + instance = getattr(completer, 'im_self', completer.__self__) + instance.rlcomplete = new_completer + + if readline.__doc__ and 'libedit' in readline.__doc__: + raise Exception('''libedit based readline is known not to work, + see etc/PROBLEMS under \"In Inferior Python mode, input is echoed\".''') + + readline.parse_and_bind('tab: complete') + # Require just one tab to send output. + readline.parse_and_bind('set show-all-if-ambiguous on') + + +__PYTHON_EL_native_completion_setup() + +def setup(init_file=None): + sys.modules['__repl__'] = sys.modules[__name__] + tl = top_level() + tl.f_globals["__name__"] = "__repl__" + tl.f_globals["pm"] = Autocall(pm) + if init_file and os.path.exists(init_file): + try: + exec(open(init_file).read(), tl.f_globals) + except: + pass + +def reload(): + import importlib.util + spec = importlib.util.spec_from_file_location('lispy-python', __file__) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + top_level().f_globals["lp"] = mod + sys._getframe().f_back.f_globals["lp"] = mod + sys._getframe().f_back.f_locals["lp"] = mod + return mod + +def reload_module(fname): + to_reload = [] + for (name, module) in sys.modules.copy().items(): + try: + if module.__dict__.get("__file__") == fname and name != "__main__": + to_reload.append((name, module)) + except: + pass + for (name, module) in to_reload: + try: + importlib.reload(module) + except: + pass + +def goto_definition(fname: str, line: int, column: int) -> None: + d = jedi.Script(path=fname).goto(line, column) + if d: + print_elisp((str(d[0].module_path), d[0].line, d[0].column)) + +def goto_link_definition(c: str) -> None: + module = c.split(".")[0] + d = jedi.Script(code=f"import {module}\n{c}").goto(2, len(c) - 2, follow_imports=True) + if d: + print_elisp((str(d[0].module_path), d[0].line, d[0].column)) + +def ast_pp(code: str) -> str: + parsed = ast.parse(code, mode="exec") + return ast.dump(parsed.body[0], indent=4) + +Expr = Union[List[ast.stmt], ast.stmt, Any] + +def has_return(p: Expr) -> bool: + if isinstance(p, list): + return any(has_return(x) for x in p) + if isinstance(p, ast.Return): + return True + elif isinstance(p, ast.If): + return has_return(p.body) or has_return(p.orelse) + else: + return False + +def tr_returns(p: Expr) -> Expr: + if isinstance(p, list): + return [tr_returns(x) for x in p] + if isinstance(p, ast.Return) and p.value: + return ast.parse("return locals() | {'__return__': " + ast.unparse(p.value) + "}").body[0] + elif isinstance(p, ast.If): + return ast.If( + test=p.test, + body=tr_returns(p.body), + orelse=tr_returns(p.orelse)) + else: + return p + +def ast_call(func: Union[str, AST], args: List[Any] = [], keywords: List[Any] = []): + if isinstance(func, str): + func = ast.Name(func) + return ast.Call(func=func, args=args, keywords=keywords) + +def wrap_return(parsed: List[ast.stmt]) -> Expr: + return [ + ast.FunctionDef( + name="__res__", + body=[*parsed, *ast.parse("return {'__return__': None}").body], + decorator_list=[], + args=[], + lineno=0, + col_offset=0), + ast.Expr( + ast_call( + ast.Attribute(value=ast_call("locals"), attr="update"), + args=[ast_call("__res__")])), + ast.Expr(value=ast.Name("__return__")) + ] + +def try_in_expr(p: Expr) -> Optional[Tuple[ast.expr, ast.expr]]: + if not isinstance(p, list): + return None + if pytest_mark := try_pytest_mark(p): + return pytest_mark + p0 = p[0] + if not isinstance(p0, ast.Expr): + return None + if not isinstance(p0.value, ast.Compare): + return None + if not isinstance(p0.value.ops[0], ast.In): + return None + return (p0.value.left, p0.value.comparators[0]) + +def select_item(code: str, idx: int, _f: Optional[FrameType] = None) -> Any: + _f = _f or sys._getframe().f_back + parsed = ast.parse(code, mode="exec").body + in_expr = try_in_expr(parsed) + assert in_expr + (left, right) = in_expr + l = ast.unparse(left) + r = ast.unparse(right) + locals_1 = locals() + locals_2 = locals_1.copy() + # pylint: disable=exec-used + exec(f"{l} = list({r})[{idx}]", _f.f_locals | _f.f_globals, locals_2) + for bind in [k for k in locals_2.keys() if k not in locals_1.keys()]: + _f.f_globals[bind] = locals_2[bind] + # pylint: disable=eval-used + return eval(l, locals_2) + +def ast_match(p: Expr, expr: Any) -> bool: + if isinstance(p, ast.Attribute): + if expr[0] != ".": + return False + return ( + expr[2] == p.attr + and ast_match(p.value, expr[1])) + elif isinstance(p, ast.Name): + return p.id == expr + elif isinstance(p, str): + return p == expr + else: + raise RuntimeError(f"Can't compare: {p} == {expr}") + +def try_pytest_mark(p: Expr) -> Optional[Expr]: + if not isinstance(p, list): + return None + if not len(p) == 1: + return None + p0 = p[0] + if not isinstance(p0, ast.FunctionDef): + return None + if not len(p0.decorator_list) == 1: + return None + decorator = p0.decorator_list[0] + if ast_match(decorator.func, (".", (".", "pytest", "mark"), "parametrize")): + assert len(decorator.args) == 2 + return [ast.Name(decorator.args[0].value), decorator.args[1]] + return None + +def to_elisp(code: str, _f: Optional[FrameType] = None) -> str: + _f = _f or top_level() + with io.StringIO() as buf, redirect_stdout(buf): + # pylint: disable=eval-used + print_elisp(eval(code, _f.f_locals | _f.f_globals)) + return buf.getvalue().strip() + +def translate(code: str, _f: Optional[FrameType] = None, use_in_expr: bool = False) -> Any: + _f = _f or sys._getframe().f_back + parsed = ast.parse(code, mode="exec").body + in_expr = try_in_expr(parsed) + if use_in_expr and in_expr: + (left, right) = in_expr + out = to_elisp(ast.unparse(right), _f) + nc = f"print('''{out}''')\n'select'" + return ast.parse(nc).body + elif has_return(parsed): + r = tr_returns(parsed) + assert isinstance(r, list) + return wrap_return(r) + else: + return parsed + +class EvalResult(TypedDict): + res: str + binds: Dict[str, str] + out: str + err: Optional[str] + +def eval_code(_code: str, _env: Dict[str, Any] = {}) -> EvalResult: + _res = "unset" + binds = {} + out = "" + err: Optional[str] = None + _f = _env.get("frame", sys._getframe().f_back) + if "fname" in _env: + _f.f_globals["__file__"] = _env["fname"] + try: + _code = _code or slurp(_env["code"]) + new_code = translate(_code, _f, _env.get("use-in-expr", False)) + (*butlast, last) = new_code + _locals = {} + locals_1 = _locals + locals_2 = locals_1.copy() + locals_globals = _f.f_locals | _f.f_globals + if "debug" in _env: + print(f"{ast.unparse(last)=}") + with io.StringIO() as buf, redirect_stdout(buf): + if butlast: + # pylint: disable=exec-used + exec(ast.unparse(butlast), locals_globals, locals_2) + for bind in [k for k in locals_2.keys() if k not in locals_1.keys()]: + _f.f_globals[bind] = locals_2[bind] + try: + # pylint: disable=eval-used + _res = eval(ast.unparse(last), locals_globals, locals_2) + except SyntaxError: + locals_1 = _locals + locals_2 = locals_1.copy() + exec(ast.unparse(last), locals_globals, locals_2) + out = buf.getvalue().strip() + binds1 = [k for k in locals_2.keys() if k not in locals_1.keys()] + for bind in binds1: + _f.f_globals[bind] = locals_2[bind] + binds2 = [bind for bind in binds1 if bind not in ["__res__", "__return__"]] + print_fn = cast(Callable[..., str], to_str if _env.get("echo") else str) + binds = {bind: print_fn(locals_2[bind]) for bind in binds2} + # except RuntimeError as e: + # if str(e) == "break": + # pm() + # else: + # raise + # pylint: disable=broad-except + except Exception as e: + err = f"{e.__class__.__name__}: {e}\n{e.__dict__}" + _f.f_globals["e"] = e + locs = e.__traceback__.tb_frame.f_locals.get("locals_2", {}) + for bind in locs: + _f.f_globals[bind] = locs[bind] + return { + "res": to_str(_res) if _env.get("echo") else repr(_res), + "binds": binds, + "out": out, + "err": err + } + +def eval_to_json(code: str, env: Dict[str, Any] = {}) -> None: + try: + env["frame"] = sys._getframe().f_back + s = json.dumps(eval_code(code, env)) + print(s) + # pylint: disable=broad-except + except Exception as e: + print(json.dumps({ + "res": None, + "binds": {}, + "out": "", + "err": str(e)})) + +def find_module(fname: str) -> Optional[ModuleType]: + for (name, module) in sys.modules.items(): + if getattr(module, "__file__", None) == fname: + return module + return None + +def generate_import(code_fname: str, buffer_fname: str) -> None: + code = slurp(code_fname) + parsed = ast.parse(code).body[0] + if isinstance(parsed, ast.FunctionDef): + name = parsed.name + module = find_module(buffer_fname) + assert module + print(f"from {module.__name__} import {name}") diff --git a/lispy-test.el b/lispy-test.el index 97c4e30d..8de14f0f 100644 --- a/lispy-test.el +++ b/lispy-test.el @@ -148,9 +148,10 @@ Insert KEY if there's no command." (should (string= (lispy-with "|(c 3 (b 2 (a 1)))" (call-interactively #'lispy-toggle-thread-last)) "|(thread-last (a 1) (b 2) (c 3))")) - (should (string= (lispy-with "|(equal 1443070800.0\n (ts-unix\n (ts-parse-org-element\n (org-element-context))))" - (lispy-toggle-thread-last)) - "|(thread-last (org-element-context)\n (ts-parse-org-element)\n (ts-unix)\n (equal 1443070800.0))")) + (should (string-match-p + "|(thread-last (org-element-context)\n +(ts-parse-org-element)\n +(ts-unix)\n +(equal 1443070800.0))" + (lispy-with "|(equal 1443070800.0\n (ts-unix\n (ts-parse-org-element\n (org-element-context))))" + (lispy-toggle-thread-last)))) (should (string= (lispy-with "|(thread-last (org-element-context)\n (ts-parse-org-element)\n (ts-unix)\n (equal 1443070800.0))" (lispy-toggle-thread-last)) "|(equal 1443070800.0\n (ts-unix\n (ts-parse-org-element\n (org-element-context))))"))) @@ -2549,24 +2550,21 @@ Insert KEY if there's no command." lispy--eval-cond-msg)))) (ert-deftest lispy-eval-other-window () - (setq lispy--eval-sym nil) - (should (string= (lispy-with-v el "(dolist |(s '(1 2 3))\n (message \"val: %d\" s))" - (lispy-eval-other-window)) "1")) - (should (string= (lispy-with-v el "(dolist |(s '(1 2 3))\n (message \"val: %d\" s))" - (lispy-eval-other-window)) "2")) - (should (string= (lispy-with-v el "(dolist |(s '(1 2 3))\n (message \"val: %d\" s))" - (lispy-eval-other-window)) "3")) - (should (string= (lispy-with-v el "(dolist |(s '(1 2 3))\n (message \"val: %d\" s))" - (lispy-eval-other-window)) "nil")) - (should (string= (lispy-with-v el "(dolist |(s '(1 2 3))\n (message \"val: %d\" s))" - (lispy-eval-other-window)) "1")) - (setq lispy--eval-sym nil) - (should (string= (lispy-with-v el "(mapcar |(lambda (s) (* s s)) '(1 2))" - (lispy-eval-other-window)) "1")) - (should (string= (lispy-with-v el "(mapcar |(lambda (s) (* s s)) '(1 2))" - (lispy-eval-other-window)) "2")) - (should (string= (lispy-with-v el "(mapcar |(lambda (s) (* s s)) '(1 2))" - (lispy-eval-other-window)) "nil"))) + (unless noninteractive + (cl-labels ((p (keys) + (setq s nil) + (setq unread-command-events (listify-key-sequence (kbd keys))) + (lispy-eval-other-window))) + (should (string= (lispy-with-v el "(dolist |(s '(1 2 3))\n (message \"val: %d\" s))" + (p "C-m")) "1")) + (should (string= (lispy-with-v el "(dolist |(s '(1 2 3))\n (message \"val: %d\" s))" + (p "C-n C-m")) "2")) + (should (string= (lispy-with-v el "(dolist |(s '(1 2 3))\n (message \"val: %d\" s))" + (p "C-n C-n C-m")) "3")) + (should (string= (lispy-with-v el "(mapcar |(lambda (s) (* s s)) '(1 2))" + (p "C-m")) "1")) + (should (string= (lispy-with-v el "(mapcar |(lambda (s) (* s s)) '(1 2))" + (p "C-n C-m")) "2"))))) (ert-deftest lispy-ace-char () (should (string= (lispy-with "|(cons 'norwegian 'blue)" @@ -3267,36 +3265,18 @@ Insert KEY if there's no command." "\nif cond1:\n |if cond2:\n expr1\n if cond3:\n expr2\n else:\n expr3\n else:\n expr4\nelse:\n expr5" (lispy-eval-python-str)) "if cond2:\n expr1\n if cond3:\n expr2\n else:\n expr3\n else:\n expr4")) - (should (equal (lispy-with-v py - "|s = (\n \"this \"\n \"is \"\n \"a string\")" - (lispy-eval-python-str)) - "s = ( \"this \" \"is \" \"a string\")")) (should (equal (lispy-with-v py "|@up_down\ndef greet(name):\n return \"my oh my, {}\".format(name)\n\ndef other():\n pass" (let ((forward-sexp-function nil)) (lispy-eval-python-str))) "@up_down\ndef greet(name):\n return \"my oh my, {}\".format(name)")) - (should (equal (lispy-with-v py - "|scores = np.array([[1, 2, 3, 6],\n [2, 4, 5, 6],\n [3, 8, 7, 6]])" - (let ((forward-sexp-function nil)) - (lispy-eval-python-str))) - "scores = np.array([[1, 2, 3, 6], [2, 4, 5, 6], [3, 8, 7, 6]])")) - (should (equal (lispy-with-v py - "|scores = np.array([[1, 2, 3, 6],\\\n [2, 4, 5, 6],\\\n [3, 8, 7, 6]])" - (let ((forward-sexp-function nil)) - (lispy-eval-python-str))) - "scores = np.array([[1, 2, 3, 6], [2, 4, 5, 6], [3, 8, 7, 6]])")) (unless (version< emacs-version "24.4.1") (should (equal (progn ;; skip initialization msg - (lispy--eval-python "") + (lispy--eval-python-plain "") (sit-for 0.1) (lispy--eval-python "print(\"one\")\nprint(\"two\")\nx = 2 + 1")) - "one\ntwo\n3"))) - (should (equal (lispy-with-v py - "def func ():\n |v = Foo.bar (\n Foo.baz,\n self.comp, xrt)\n x = 0" - (lispy-eval-python-str)) - "v = Foo.bar ( Foo.baz, self.comp, xrt)"))) + "x = 3")))) (ert-deftest lispy-python-symbol-bnd () (should (equal (lispy-with-v py "def test_detector ():\n detector.getChannelCount ().|" @@ -3350,21 +3330,21 @@ Insert KEY if there's no command." ;; (should (string= (lispy--python-eval-string-dwim "(x, i) in enumerate(lvl_npoints)") ;; "(x, i) = list (enumerate(lvl_npoints))[0]\nprint (((x, i)))")) (should (string= (lispy--python-eval-string-dwim "asdf_123") - "print(repr(asdf_123))")) + "print(repr((asdf_123)))")) (should (string= (let ((this-command 'lispy-eval)) (lispy--python-eval-string-dwim "asdf_123")) - "lp.pprint(asdf_123)")) + "lp.pprint((asdf_123))")) (should (string= (let ((this-command 'lispy-eval)) (lispy--python-eval-string-dwim "x[\"foo\"] = 2 + 2")) - "x[\"foo\"] = 2 + 2\nlp.pprint(x[\"foo\"])"))) + "x[\"foo\"] = 2 + 2\nlp.pprint((x[\"foo\"]))"))) (ert-deftest lispy-extended-eval-str () - (should (string= - (lispy-with-v py - "|x = (\n \"abc\" +\n \"def\" +\n \"foo\" + \"bar\")" - (lispy-extended-eval-str - (cons (line-beginning-position) (line-end-position)))) - "x = ( \"abc\" + \"def\" + \"foo\" + \"bar\")")) + ;; (should (string= + ;; (lispy-with-v py + ;; "|x = (\n \"abc\" +\n \"def\" +\n \"foo\" + \"bar\")" + ;; (lispy-extended-eval-str + ;; (cons (line-beginning-position) (line-end-position)))) + ;; "x = ( \"abc\" + \"def\" + \"foo\" + \"bar\")")) (should (string= (lispy-with-v py "|print(\"((\", end=\"\")\nprint(\" \".join(['\"' + arg + '\"' for arg in arg_info.args]))" diff --git a/lispy.el b/lispy.el index 5ba27b1e..f2322482 100644 --- a/lispy.el +++ b/lispy.el @@ -140,6 +140,7 @@ (require 'ediff-util) (require 'semantic) (require 'semantic/db)) +(require 'zoutline) (require 'mode-local) (require 'lispy-tags) (require 'help-fns) @@ -151,7 +152,6 @@ (require 'lispy-inline) (setq iedit-toggle-key-default nil) (require 'delsel) -(require 'swiper) (require 'pcase) (require 'hydra) (eval-after-load 'cider '(require 'le-clojure)) @@ -219,7 +219,7 @@ The hint will consist of the possible nouns that apply to the verb." :group 'lispy) (defcustom lispy-close-quotes-at-end-p nil - "If t, when pressing the `\"' at the end of a quoted string, it will move you past the end quote." + "If t, when pressing `\"' at the end of a quoted string, move past the end quote." :type 'boolean :group 'lispy) @@ -278,6 +278,12 @@ The hint will consist of the possible nouns that apply to the verb." "Keys for jumping." :type '(repeat :tag "Keys" (character :tag "char"))) +(declare-function counsel-imenu "ext:counsel") + +(defcustom lispy-imenu-function #'counsel-imenu + "Function used to jump to an imenu entry." + :type 'function) + (defface lispy-command-name-face '((((class color) (background light)) :background "#d8d8f7" :inherit font-lock-function-name-face) @@ -451,6 +457,13 @@ Depends on `default-directory'." (lispy--normalize-files (all-completions "" #'read-file-name-internal))))))) +(defconst lispy-font-lock-keywords + '(("^;; ?\\(\\*\\|\\*[^*\n].*\\)$" 1 'org-level-1 prepend) + ("^;; ?\\(\\*\\{2\\}\\|\\*\\{2\\}[^*\n].*\\)$" 1 'org-level-2 prepend) + ("^;; ?\\(\\*\\{3\\}\\|\\*\\{3\\}[^*\n].*\\)$" 1 'org-level-3 prepend) + ("^;; ?\\(\\*\\{4\\}\\|\\*\\{4\\}[^*\n].*\\)$" 1 'org-level-4 prepend) + ("^;; ?\\(\\*\\{5\\}\\|\\*\\{5\\}[^*\n].*\\)$" 1 'org-level-5 prepend))) + ;;;###autoload (define-minor-mode lispy-mode "Minor mode for navigating and editing LISP dialects. @@ -492,8 +505,8 @@ backward through lists, which is useful to move into special. ((eq major-mode 'clojure-mode) (eval-after-load 'le-clojure '(setq completion-at-point-functions - '(lispy-clojure-complete-at-point - cider-complete-at-point))) + '(lispy-clojure-complete-at-point + cider-complete-at-point))) (setq-local outline-regexp (substring lispy-outline 1))) ((eq major-mode 'python-mode) (setq-local lispy-outline "^#\\*+") @@ -504,11 +517,15 @@ backward through lists, which is useful to move into special. (setq-local outline-regexp (substring lispy-outline 1)))) (when (called-interactively-p 'any) (mapc #'lispy-raise-minor-mode - (cons 'lispy-mode lispy-known-verbs)))) + (cons 'lispy-mode lispy-known-verbs))) + (font-lock-add-keywords major-mode lispy-font-lock-keywords)) (when lispy-old-outline-settings (setq outline-regexp (car lispy-old-outline-settings)) (setq outline-level (cdr lispy-old-outline-settings)) - (setq lispy-old-outline-settings nil)))) + (setq lispy-old-outline-settings nil)) + (font-lock-remove-keywords major-mode lispy-font-lock-keywords))) + + (defun lispy-raise-minor-mode (mode) "Make MODE the first on `minor-mode-map-alist'." @@ -581,6 +598,9 @@ Otherwise return the amount of times executed." (nthcdr (1- ,n) (prog1 ,lst (setq ,lst (nthcdr ,n ,lst)))) nil)))) +(defvar lispy-whitespace-types '(clojure-commas) + "List of tokens to treat as whitespace") + (defvar lispy-site-directory (file-name-directory load-file-name) "The directory where all of the lispy files are located.") @@ -1138,7 +1158,10 @@ If position isn't special, move to previous or error." (line-beginning-position))) (delete-char 1) (backward-delete-char 1)) - ((lispy-left-p) + ((and (lispy-left-p) + (if (memq major-mode lispy-elisp-modes) + (not (eq (char-after) ?\{)) + t)) (if (progn (setq bnd (lispy--bounds-list)) (> (count-lines (car bnd) (cdr bnd)) 1)) @@ -1162,7 +1185,7 @@ If position isn't special, move to previous or error." (while (and (< (point) end) (ignore-errors (forward-sexp 1) - (skip-chars-forward " ") + (skip-chars-forward " ,") t)) (when (setq bnd (lispy--bounds-comment)) (goto-char (cdr bnd)))) @@ -1199,8 +1222,8 @@ If position isn't special, move to previous or error." 'kill-region last-command))) (lispy-dotimes arg - (when (lispy--in-comment-p) - (skip-chars-backward " \n")) + (when (and (lispy--in-comment-p) (looking-back ";[ \n]+" (point-min))) + (goto-char (1+ (match-beginning 0)))) (if (memq (char-syntax (char-before)) '(?w ?_ ?\s)) (if (lispy-looking-back "\\_<\\s_+") @@ -1667,26 +1690,29 @@ When ARG is more than 1, mark ARGth element." (t (lispy--mark (lispy--bounds-dwim)))))) +(defvar lispy-kill-at-point-hook nil) + (defun lispy-kill-at-point () "Kill the quoted string or the list that includes the point." (interactive) - (cond ((region-active-p) - (lispy--maybe-safe-kill-region (region-beginning) - (region-end))) - ((derived-mode-p 'text-mode) - (let ((beg (save-excursion - (1+ (re-search-backward "[ \t\n]" nil t)))) - (end (save-excursion - (1- (re-search-forward "[ \t\n]" nil t))))) - (kill-region beg end))) - (t - (let ((bounds (or (lispy--bounds-comment) - (lispy--bounds-string) - (lispy--bounds-list)))) - (if buffer-read-only - (kill-new (buffer-substring - (car bounds) (cdr bounds))) - (kill-region (car bounds) (cdr bounds))))))) + (cond + ((run-hook-with-args-until-success 'lispy-kill-at-point-hook)) + ((region-active-p) + (lispy--maybe-safe-kill-region (region-beginning) + (region-end))) + (t + (let ((bnd (or (lispy--bounds-comment) + (lispy--bounds-string) + (lispy--bounds-list) + (and (derived-mode-p 'text-mode) + (cons (save-excursion + (1+ (re-search-backward "[ \t\n]" nil t))) + (save-excursion + (1- (re-search-forward "[ \t\n]" nil t)))))))) + (if buffer-read-only + (kill-new (buffer-substring + (car bnd) (cdr bnd))) + (kill-region (car bnd) (cdr bnd))))))) (defun lispy-new-copy () "Copy marked region or sexp to kill ring." @@ -1868,7 +1894,7 @@ otherwise the whole string is unquoted." (self-insert-command 1)) (t - (lispy--space-unless "^\\|\\s-\\|\\s(\\|[#]") + (lispy--space-unless "^\\|\\s-\\|\\s(\\|[#][a-zA-Z]?") (insert "\"\"") (unless (looking-at "\n\\|)\\|}\\|\\]\\|$") (just-one-space) @@ -1949,7 +1975,8 @@ colon following `lispy-colon-no-space-regex'. To disable this behavior, set this variable to nil.") (defvar lispy-colon-no-space-regex - '((lisp-mode . "\\s-\\|[:^?#]\\|ql\\|\\(?:\\s([[:word:]-]*\\)")) + '((lisp-mode . "\\s-\\|[:^?#]\\|ql\\|\\(?:\\(\\s(\\|'\\)[[:word:]-]*\\)") + (slime-repl-mode . "")) "Overrides REGEX that `lispy-colon' will consider for `major-mode'. `lispy-colon' will insert \" :\" instead of \":\" unless `lispy-no-space' is t or `looking-back' REGEX.") @@ -2073,6 +2100,7 @@ When the region is active, toggle a ~ at the start of the region." (declare-function slime-repl-return "ext:slime-repl") (declare-function sly-mrepl-return "ext:sly-mrepl") (declare-function racket-repl-submit "ext:racket-repl") +(declare-function geiser-repl-maybe-send "ext:geiser-repl") (defun lispy-newline-and-indent-plain () "When in minibuffer, exit it. Otherwise forward to `newline-and-indent'." (interactive) @@ -2094,6 +2122,8 @@ When the region is active, toggle a ~ at the start of the region." (ielm-return)) (racket-repl-mode (racket-repl-submit)) + (geiser-repl-mode + (geiser-repl-maybe-send)) (t (if (and (not (lispy--in-string-or-comment-p)) (if (memq major-mode lispy-clojure-modes) @@ -2130,10 +2160,13 @@ When ARG is nagative, add them above instead" (defun lispy-meta-return () "Insert a new heading." (interactive) - (let ((pt (point))) + (let ((pt (point)) + (lvl (lispy-outline-level))) (cond ((lispy--in-comment-p) - (end-of-line) - (newline)) + (goto-char (cdr (zo-bnd-subtree))) + (when (looking-back "\n+" (point-min)) + (delete-region (match-beginning 0) (match-end 0))) + (insert "\n\n")) ((and (lispy-bolp) (looking-at " *$")) (delete-region @@ -2148,12 +2181,11 @@ When ARG is nagative, add them above instead" (forward-list 1) (newline)) (newline) - (backward-char 1))))) - (insert lispy-outline-header - (make-string (max (lispy-outline-level) 1) - ?\*) - " ") - (beginning-of-line)) + (backward-char 1)))) + (insert lispy-outline-header + (make-string (max lvl 1) ?\*) + " ") + (beginning-of-line))) (defun lispy-alt-line (&optional N) "Do a context-aware exit, then `newline-and-indent', N times. @@ -2232,197 +2264,6 @@ to all the functions, while maintaining the parens in a pretty state." (iedit-mode 0) (iedit-mode)))) -;;* Locals: navigation -;;** Occur -(defcustom lispy-occur-backend 'ivy - "Method to navigate to a line with `lispy-occur'." - :type '(choice - (const :tag "Ivy" ivy) - (const :tag "Helm" helm))) - -(defvar lispy--occur-beg 1 - "Start position of the top level sexp during `lispy-occur'.") - -(defvar lispy--occur-end 1 - "End position of the top level sexp during `lispy-occur'.") - -(defun lispy--occur-candidates (&optional bnd) - "Return the candidates for `lispy-occur'." - (setq bnd (or bnd (save-excursion - (unless (and (bolp) - (lispy-left-p)) - (beginning-of-defun)) - (lispy--bounds-dwim)))) - (let ((line-number -1) - candidates) - (setq lispy--occur-beg (car bnd)) - (setq lispy--occur-end (cdr bnd)) - (save-excursion - (goto-char lispy--occur-beg) - (while (< (point) lispy--occur-end) - (push (format "%-3d %s" - (cl-incf line-number) - (buffer-substring - (line-beginning-position) - (line-end-position))) - candidates) - (forward-line 1))) - (nreverse candidates))) - -(defun lispy--occur-preselect () - "Initial candidate regex for `lispy-occur'." - (format "^%d" - (- - (line-number-at-pos (point)) - (line-number-at-pos lispy--occur-beg)))) - -(defvar helm-input) -(declare-function helm "ext:helm") - -(defun lispy-occur-action-goto-paren (x) - "Goto line X for `lispy-occur'." - (setq x (read x)) - (goto-char lispy--occur-beg) - (let ((input (if (eq lispy-occur-backend 'helm) - helm-input - ivy-text)) - str-or-comment) - (cond ((string= input "") - (forward-line x) - (back-to-indentation) - (when (re-search-forward lispy-left (line-end-position) t) - (goto-char (match-beginning 0)))) - - ((setq str-or-comment - (progn - (forward-line x) - (re-search-forward (ivy--regex input) - (line-end-position) t) - (lispy--in-string-or-comment-p))) - (goto-char str-or-comment)) - - ((re-search-backward lispy-left (line-beginning-position) t) - (goto-char (match-beginning 0))) - - ((re-search-forward lispy-left (line-end-position) t) - (goto-char (match-beginning 0))) - - (t - (back-to-indentation))))) - -(defun lispy-occur-action-goto-end (x) - "Goto line X for `lispy-occur'." - (setq x (read x)) - (goto-char lispy--occur-beg) - (forward-line x) - (re-search-forward (ivy--regex ivy-text) (line-end-position) t)) - -(defun lispy-occur-action-goto-beg (x) - "Goto line X for `lispy-occur'." - (when (lispy-occur-action-goto-end x) - (goto-char (match-beginning 0)))) - -(defun lispy-occur-action-mc (_x) - "Make a fake cursor for each `lispy-occur' candidate." - (let ((cands (nreverse ivy--old-cands)) - cand) - (while (setq cand (pop cands)) - (goto-char lispy--occur-beg) - (forward-line (read cand)) - (re-search-forward (ivy--regex ivy-text) (line-end-position) t) - (when cands - (mc/create-fake-cursor-at-point)))) - (multiple-cursors-mode 1)) - -(ivy-set-actions - 'lispy-occur - '(("m" lispy-occur-action-mc "multiple-cursors") - ("j" lispy-occur-action-goto-beg "goto start") - ("k" lispy-occur-action-goto-end "goto end"))) - -(defvar ivy-last) -(declare-function ivy-state-window "ext:ivy") - -(defun lispy-occur () - "Select a line within current top level sexp. -See `lispy-occur-backend' for the selection back end." - (interactive) - (swiper--init) - (cond ((eq lispy-occur-backend 'helm) - (require 'helm) - (add-hook 'helm-move-selection-after-hook - #'lispy--occur-update-input-helm) - (add-hook 'helm-update-hook - #'lispy--occur-update-input-helm) - (unwind-protect - (helm :sources - `((name . "this defun") - (candidates . ,(lispy--occur-candidates)) - (action . lispy-occur-action-goto-paren) - (match-strict . - (lambda (x) - (ignore-errors - (string-match - (ivy--regex helm-input) x))))) - :preselect (lispy--occur-preselect) - :buffer "*lispy-occur*") - (swiper--cleanup) - (remove-hook 'helm-move-selection-after-hook - #'lispy--occur-update-input-helm) - (remove-hook 'helm-update-hook - #'lispy--occur-update-input-helm))) - ((eq lispy-occur-backend 'ivy) - (unwind-protect - (ivy-read "pattern: " - (lispy--occur-candidates) - :preselect (lispy--occur-preselect) - :require-match t - :update-fn (lambda () - (lispy--occur-update-input - ivy-text - (ivy-state-current ivy-last))) - :action #'lispy-occur-action-goto-paren - :caller 'lispy-occur) - (swiper--cleanup) - (when (null ivy-exit) - (goto-char swiper--opoint)))) - (t - (error "Bad `lispy-occur-backend': %S" lispy-occur-backend)))) - -(defun lispy--occur-update-input-helm () - "Update selection for `lispy-occur' using `helm' back end." - (lispy--occur-update-input - helm-input - (buffer-substring-no-properties - (line-beginning-position) - (line-end-position)))) - -(defun lispy--occur-update-input (input str) - "Update selection for `ivy-occur'. -INPUT is the current input text. -STR is the full current candidate." - (swiper--cleanup) - (let ((re (ivy--regex input)) - (num (if (string-match "^[0-9]+" str) - (string-to-number (match-string 0 str)) - 0))) - (with-selected-window (ivy-state-window ivy-last) - (goto-char lispy--occur-beg) - (when (cl-plusp num) - (forward-line num) - (unless (<= (point) lispy--occur-end) - (recenter))) - (let ((ov (make-overlay (line-beginning-position) - (1+ (line-end-position))))) - (overlay-put ov 'face 'swiper-line-face) - (overlay-put ov 'window (ivy-state-window ivy-last)) - (push ov swiper--overlays)) - (re-search-forward re (line-end-position) t) - (swiper--add-overlays - re - lispy--occur-beg - lispy--occur-end)))) - ;;* Locals: Paredit transformations (defun lispy--sub-slurp-forward (arg) "Grow current marked symbol by ARG words forwards. @@ -2630,7 +2471,11 @@ If indenting does not adjust indentation or move the point, call (bnd (when (region-active-p) (cons (region-beginning) (region-end))))) - (indent-for-tab-command) + ;; the current TAB may not always be `indent-for-tab-command' + (cond + ((memq major-mode '(minibuffer-mode minibuffer-inactive-mode)) + (completion-at-point)) + (t (indent-for-tab-command))) (when (and (= tick (buffer-chars-modified-tick)) (= pt (point))) (if bnd @@ -3632,8 +3477,8 @@ When ARG is `fill', do nothing for short expressions." (lispy--insert res)))))) (defvar-local lispy--multiline-take-3 - '(defvar defun defmacro defcustom defgroup defvar-local declare-function - define-key nth throw define-error defadvice defhydra defsubst) + '(defvar defun defmacro defcustom defgroup defvar-local declare-function + define-key nth throw define-error defadvice defhydra defsubst) "List of constructs for which the first 3 elements are on the first line.") (setq-mode-local @@ -3677,18 +3522,31 @@ The third one is assumed to be the arglist and will not be changed.") "List of constructs for which the first 2 elements are on the first line. The second one will not be changed.") -(defun lispy-interleave (x lst &optional step) +(defmacro lispy--slurp-whitespace (from to) + "Move any leading whitespace in FROM into TO." + `(while (member (cadar ,from) lispy-whitespace-types) + (setq ,to (cons (car ,from) ,to)) + (setq ,from (cdr ,from)))) + +(defun lispy-interleave (x lst &optional step slurp-whitespace) "Insert X in between each element of LST. Don't insert X when it's already there. -When STEP is non-nil, insert in between each STEP elements instead." +When STEP is non-nil, insert in between each STEP elements instead. +When SLURP-WHITESPACE is non-nil, add any whitespace following split into previous line." (setq step (or step 1)) (let ((res (nreverse (lispy-multipop lst step))) item) + (if slurp-whitespace + (lispy--slurp-whitespace lst res)) (while lst (unless (equal (car res) x) (push x res)) (unless (equal (car res) (car (setq item (lispy-multipop lst step)))) + (when slurp-whitespace + (setq item (nreverse item)) + (lispy--slurp-whitespace lst item) + (setq item (nreverse item))) (setq res (nconc (nreverse item) res)))) (nreverse res))) @@ -3726,7 +3584,8 @@ When QUOTED is not nil, assume that EXPR is quoted and ignore some rules." (list 'ly-raw (cadr expr) (lispy-interleave '(ly-raw newline) (mapcar #'lispy--multiline-1 (cl-caddr expr)) - 2))) + 2 + t))) ((and (eq (car-safe expr) 'ly-raw) (eq (nth 1 expr) 'splice)) (list 'ly-raw (nth 1 expr) (nth 2 expr) (nth 3 expr) @@ -4193,8 +4052,6 @@ When ARG isn't nil, call `lispy-goto-projectile' instead." (mapcar #'lispy--format-tag-line candidates)) #'lispy--action-jump))) -(declare-function counsel-imenu "ext:counsel") - (defun lispy-goto-local (&optional arg) "Jump to symbol within current file. When ARG is non-nil, force a reparse." @@ -4207,7 +4064,7 @@ When ARG is non-nil, force a reparse." (lispy--fetch-tags (list (buffer-file-name)))) #'lispy--action-jump)) (no-semantic-support - (counsel-imenu)))) + (funcall lispy-imenu-function)))) (defun lispy-goto-elisp-commands (&optional arg) "Jump to Elisp commands within current file. @@ -4286,8 +4143,7 @@ SYMBOL is a string." (lispy--current-function)))) (lispy--remember) (deactivate-mark) - (with-no-warnings - (ring-insert find-tag-marker-ring (point-marker))) + (xref-push-marker-stack) (let ((narrowedp (buffer-narrowed-p))) (when narrowedp (widen)) @@ -4353,11 +4209,13 @@ SYMBOL is a string." le-clojure lispy-eval-clojure) (python-mode le-python lispy--eval-python lispy-eval-python-str lispy-eval-python-bnd) + (js2-mode + le-js lispy--eval-js lispy--eval-js-str) (julia-mode le-julia lispy-eval-julia lispy-eval-julia-str) (racket-mode le-racket lispy--eval-racket) - (scheme-mode + ((scheme-mode geiser-repl-mode) le-scheme lispy--eval-scheme) (lisp-mode le-lisp lispy--eval-lisp) @@ -4373,7 +4231,7 @@ SYMBOL is a string." (define-error 'eval-error "Eval error") -(defun lispy-eval (arg) +(defun lispy-eval (arg &optional e-str) "Eval the current sexp and display the result. When ARG is 2, insert the result as a comment. When at an outline, eval the outline." @@ -4386,7 +4244,7 @@ When at an outline, eval the outline." (looking-at lispy-outline-header)) (lispy-eval-outline)) (t - (let ((res (lispy--eval nil))) + (let ((res (lispy--eval e-str))) (when (memq major-mode lispy-clojure-modes) (setq res (lispy--clojure-pretty-string res))) (when lispy-eval-output @@ -4493,38 +4351,46 @@ Return the result of the last evaluation as a string." (cons beg end)))) (defun lispy-eval-single-outline () - (let* ((pt (point)) - (bnd (lispy--eval-bounds-outline)) - (res - (lispy--eval-dwim bnd))) - (when lispy-eval-output - (setq res (concat lispy-eval-output res))) - (cond ((equal res "") - (message "(ok)")) - ((= ?: (char-before (line-end-position))) - (goto-char (cdr bnd)) - (save-restriction - (narrow-to-region - (point) - (if (re-search-forward outline-regexp nil t) - (1- (match-beginning 0)) - (point-max))) - (goto-char (point-min)) - (unless (looking-at (concat "\n" lispy-outline-header)) - (newline)) - (lispy--insert-eval-result res)) - (goto-char pt) - res) - (t - (message (replace-regexp-in-string "%" "%%" res)))))) + (condition-case e + (let* ((pt (point)) + (bnd (lispy--eval-bounds-outline)) + (res + (lispy--eval-dwim bnd t))) + (when lispy-eval-output + (unless (looking-at ".*::$") + (setq res (concat lispy-eval-output res)))) + (cond ((equal res "") + (message "(ok)")) + ((= ?: (char-before (line-end-position))) + (goto-char (cdr bnd)) + (save-restriction + (narrow-to-region + (point) + (if (re-search-forward outline-regexp nil t) + (1- (match-beginning 0)) + (point-max))) + (goto-char (point-min)) + (unless (looking-at (concat "\n" lispy-outline-header)) + (newline)) + (lispy--insert-eval-result res)) + (goto-char pt) + res) + (t + (lispy-message (replace-regexp-in-string "%" "%%" res))))) + + (eval-error + (lispy-message (cdr e))))) (defvar lispy-message-limit 4000 "String length limit for `lispy-message' to pop up a window. For smaller strings `message' is used.") +(defvar lispy-last-message nil) + (defun lispy-message (str &optional popup) "Display STR in the echo area. If STR is too large, pop it to a buffer instead." + (setq lispy-last-message str) (if (or popup (> (length str) lispy-message-limit) @@ -4536,8 +4402,9 @@ If STR is too large, pop it to a buffer instead." (special-mode) (let ((inhibit-read-only t)) (delete-region (point-min) (point-max)) - (insert str) - (ignore-errors (pp-buffer)) + (insert (ansi-color-apply str)) + ;; (unless (> (length str) 2000) + ;; (ignore-errors (pp-buffer))) (goto-char (point-min)) (while (re-search-forward "\\\\n" nil t) (replace-match "\n" nil t)) @@ -4547,18 +4414,22 @@ If STR is too large, pop it to a buffer instead." (message str) (error (message (replace-regexp-in-string "%" "%%" str)))))) -(defun lispy--eval-dwim (&optional bnd) - (let ((eval-str - (cond (bnd - (lispy--string-dwim bnd)) - ((eq major-mode 'python-mode) - (lispy-eval-python-str)) - (t - (lispy--string-dwim))))) - (if (string= "" eval-str) +(defun lispy--eval-dwim (&optional bnd outline-p) + (let* ((eval-str-1 + (cond (bnd + (lispy--string-dwim bnd)) + ((eq major-mode 'python-mode) + (lispy-eval-python-str)) + (t + (lispy--string-dwim)))) + (eval-str-2 + (if (and outline-p (eq major-mode 'lisp-mode)) + (concat "(progn " eval-str-1 ")") + eval-str-1))) + (if (string= "" eval-str-1) "" (condition-case e - (lispy--eval eval-str) + (lispy--eval eval-str-2) (eval-error (cdr e)))))) (defun lispy-eval-and-insert () @@ -4655,23 +4526,24 @@ If STR is too large, pop it to a buffer instead." (eq major-mode 'python-mode)) (cond ((< (current-column) 100)) ((looking-back "[]}]" (line-beginning-position)) - (let ((cnt (if (string= "]" (match-string 0)) - -1 - -2)) - (beg (save-excursion - (forward-list -1) - (1+ (point))))) - (backward-char 1) - (ignore-errors - (while (> (point) beg) - (if (lispy-after-string-p ">") - (progn - (re-search-backward "<" nil t) - (newline-and-indent) - (backward-char 3)) - (forward-sexp cnt) - (when (> (point) beg) - (newline-and-indent)))))) + (if (string= "]" (match-string 0)) + (let ((beg (save-excursion + (forward-list -1) + (1+ (point))))) + (backward-char 1) + (ignore-errors + (while (> (point) beg) + (if (lispy-after-string-p ">") + (progn + (re-search-backward "<" nil t) + (newline-and-indent) + (backward-char 3)) + (forward-sexp -1) + (when (> (point) beg) + (newline-and-indent)))))) + (goto-char (point-min)) + (while (re-search-forward ", " nil t) + (newline-and-indent))) (goto-char (point-max))) ((and (looking-back "[])]\\]" (line-beginning-position)) (eq (point-min) @@ -4739,6 +4611,10 @@ Unlike `comment-region', ensure a contiguous comment." (format "%s: nil" (propertize "cond" 'face 'font-lock-keyword-face)) "Message to echo when the current `cond' branch is nil.") +(defconst lispy--eval-pcase-msg + (format "%s: nil" (propertize "pcase" 'face 'font-lock-keyword-face)) + "Message to echo when the current `pcase' branch is nil.") + (defvar lispy-eval-other--window nil "Target window for `lispy-eval-other-window'.") @@ -4761,18 +4637,25 @@ Unlike `comment-region', ensure a contiguous comment." (declare-function aw-select "ext:ace-window") (defvar aw-dispatch-always) +(defun lispy--idx-from-list (lst &optional preselect) + (read + (ivy-read "idx: " + (cl-mapcar + (lambda (x i) + (concat (number-to-string i) + " " + (if (stringp x) + x + (prin1-to-string x)))) + lst + (number-sequence 0 (1- (length lst)))) + :preselect preselect))) + (defun lispy--set-sym-from-list (sym lst) - (let ((idx (read (ivy-read "idx: " - (cl-mapcar - (lambda (x i) - (concat (number-to-string i) - " " - (if (stringp x) - x - (prin1-to-string x)))) - lst - (number-sequence 0 (1- (length lst)))) - :preselect (and (boundp sym) (cl-position (symbol-value sym) lst)))))) + (let ((idx + (lispy--idx-from-list + lst + (and (boundp sym) (cl-position (symbol-value sym) lst))))) (set sym (nth idx lst)))) (defun lispy--dolist-item-expr (expr) @@ -4792,6 +4675,22 @@ SYM will take on each value of LST with each eval." (car lmda)))) (lispy--set-sym-from-list sym lst))) +(defun lispy--print-object (res) + (cond ((member res (list lispy--eval-cond-msg + lispy--eval-pcase-msg)) + (lispy-message res)) + ((and (fboundp 'object-p) (object-p res)) + (message "(eieio object length %d)" (length res))) + ((and (consp res) + (numberp (car res)) + (numberp (cdr res))) + (lispy-message + (format "%S\n%s" res + (lispy--string-dwim res)))) + (t + (lispy-message + (lispy--prin1 res))))) + (defun lispy-eval-other-window (&optional arg) "Eval current expression in the context of other window. In case the point is on a let-bound variable, add a `setq'. @@ -4817,28 +4716,18 @@ When ARG is non-nil, force select the window." (setq lispy-eval-other--cfg nil) (selected-window)))) res) - (cond ((memq major-mode '(lisp-mode scheme-mode)) - (lispy-message (lispy--eval (prin1-to-string expr)))) - ((memq major-mode lispy-clojure-modes) - (lispy-eval 1)) - (t - (with-selected-window target-window - (setq res (lispy--eval-elisp-form expr lexical-binding))) - (cond ((equal res lispy--eval-cond-msg) - (lispy-message res)) - ((and (fboundp 'object-p) (object-p res)) - (message "(eieio object length %d)" (length res))) - ((and (memq major-mode lispy-elisp-modes) - (consp res) - (numberp (car res)) - (numberp (cdr res))) - (lispy-message - (format "%S\n%s" res - (with-selected-window target-window - (lispy--string-dwim res))))) - (t - (lispy-message - (lispy--prin1 res)))))))) + (condition-case e + (cond ((memq major-mode '(lisp-mode scheme-mode)) + (lispy-message (lispy--eval (prin1-to-string expr)))) + ((memq major-mode lispy-clojure-modes) + (lispy-eval 1)) + (t + (with-selected-window target-window + (setq res (lispy--eval-elisp-form expr lexical-binding)) + (kill-new (if (stringp res) res (prin1-to-string res)) t) + (lispy--print-object res)))) + (eval-error + (message "%S" e))))) (defun lispy-follow () "Follow to `lispy--current-function'." @@ -5522,6 +5411,7 @@ Macro used may be customized in `lispy-thread-last-macro', which see." (iedit-start (iedit-regexp-quote (lispy--string-dwim)) beg end) (lispy-mark-symbol) (lispy-move-down 1) + (iedit-update-occurrences) (iedit-mode) (deactivate-mark) (lispy-left 1) @@ -5579,7 +5469,7 @@ The bindings of `lispy-backward' or `lispy-mark-symbol' can also be used." (let* ((bnd (lispy--bounds-dwim)) (str (lispy--string-dwim bnd)) (kind (lispy--bind-variable-kind)) - (fmt (if (eq major-mode 'clojure-mode) + (fmt (if (memq major-mode lispy-clojure-modes) '("(let [ %s]\n)" . 6) '("(let (( %s))\n)" . 7)))) (setq lispy-bind-var-in-progress t) @@ -5882,52 +5772,57 @@ ARG is 4: `eval-defun' on the function from this sexp." (let* ((ldsi-sxp (lispy--setq-expression)) (ldsi-fun (car ldsi-sxp))) (cond - ((memq ldsi-fun '(mapcar mapc mapcan - cl-remove-if cl-remove-if-not - cl-find-if cl-find-if-not - cl-some cl-every cl-any cl-notany)) - (let ((fn (nth 1 ldsi-sxp)) - (lst (nth 2 ldsi-sxp))) - (when (eq (car-safe fn) 'lambda) - (set (car (cadr fn)) (car (eval lst))) - (lispy-flow 2)))) - ((or (functionp ldsi-fun) - (macrop ldsi-fun)) - (when (eq ldsi-fun 'funcall) - (setq ldsi-fun (eval (cadr ldsi-sxp))) - (setq ldsi-sxp (cons ldsi-fun (cddr ldsi-sxp)))) - (let ((ldsi-args - (copy-sequence - (help-function-arglist - (if (ad-is-advised ldsi-fun) - (ad-get-orig-definition ldsi-fun) - ldsi-fun) - t))) - (ldsi-vals (cdr ldsi-sxp)) - ldsi-arg - ldsi-val) - (catch 'done - (while (setq ldsi-arg (pop ldsi-args)) - (cond ((eq ldsi-arg '&optional) - (setq ldsi-arg (pop ldsi-args)) - (set ldsi-arg (eval (pop ldsi-vals)))) - ((eq ldsi-arg '&rest) - (setq ldsi-arg (pop ldsi-args)) - (set ldsi-arg - (if (functionp ldsi-fun) - (mapcar #'eval ldsi-vals) - ldsi-vals)) - (throw 'done t)) - (t - (setq ldsi-val (pop ldsi-vals)) - (set ldsi-arg - (if (functionp ldsi-fun) - (eval ldsi-val) - ldsi-val)))))) - (lispy-goto-symbol ldsi-fun))) - (t - (lispy-complain - (format "%S isn't a function" ldsi-fun)))))) + ((memq ldsi-fun '(mapcar mapc mapcan + cl-remove-if cl-remove-if-not + cl-find-if cl-find-if-not + cl-some cl-every cl-any cl-notany)) + (let ((fn (nth 1 ldsi-sxp)) + (lst (nth 2 ldsi-sxp))) + (when (eq (car-safe fn) 'lambda) + (set (car (cadr fn)) (car (eval lst))) + (lispy-flow 2)))) + ((or (functionp ldsi-fun) + (macrop ldsi-fun)) + (cond ((eq ldsi-fun 'funcall) + (setq ldsi-fun (eval (cadr ldsi-sxp))) + (setq ldsi-sxp (cons ldsi-fun (cddr ldsi-sxp)))) + ((eq ldsi-fun 'apply) + (setq ldsi-fun (eval (nth 1 ldsi-sxp))) + (let ((vals (mapcar #'eval (cddr ldsi-sxp)))) + (setq ldsi-sxp + (cons ldsi-fun (append (butlast vals) (car (last vals)))))))) + (let ((ldsi-args + (copy-sequence + (help-function-arglist + (if (ad-is-advised ldsi-fun) + (ad-get-orig-definition ldsi-fun) + ldsi-fun) + t))) + (ldsi-vals (cdr ldsi-sxp)) + ldsi-arg + ldsi-val) + (catch 'done + (while (setq ldsi-arg (pop ldsi-args)) + (cond ((eq ldsi-arg '&optional) + (setq ldsi-arg (pop ldsi-args)) + (set ldsi-arg (eval (pop ldsi-vals)))) + ((eq ldsi-arg '&rest) + (setq ldsi-arg (pop ldsi-args)) + (set ldsi-arg + (if (functionp ldsi-fun) + (mapcar #'eval ldsi-vals) + ldsi-vals)) + (throw 'done t)) + (t + (setq ldsi-val (pop ldsi-vals)) + (set ldsi-arg + (if (functionp ldsi-fun) + (eval ldsi-val) + ldsi-val)))))) + (lispy-goto-symbol ldsi-fun))) + (t + (lispy-complain + (format "%S isn't a function" ldsi-fun)))))) ((eq major-mode 'clojure-mode) (require 'le-clojure) (lispy--clojure-debug-step-in)) @@ -5976,9 +5871,9 @@ An equivalent of `cl-destructuring-bind'." (interactive) (if (eq major-mode 'python-mode) (let* ((pwd - (lispy--eval-python "import os; print(os.getcwd())")) + (lispy--eval-python-plain "import os; print(os.getcwd())")) (cwd (read-directory-name "cd: " pwd))) - (lispy--eval-python (format "os.chdir('%s')" cwd)) + (lispy--eval-python-plain (format "os.chdir('%s')" cwd)) (message cwd)) (user-error "Unimplemented for %S" major-mode))) @@ -6886,17 +6781,20 @@ The result is a string." lispy-eval-alist)))) (if handler (progn + (require (nth 0 handler)) (setq e-str (or e-str (if (> (length handler) 2) (funcall (nth 2 handler)) (save-excursion (unless (or (lispy-right-p) (region-active-p)) - (lispy-forward 1)) + (let ((lispy-ignore-whitespace t)) + (lispy-forward 1))) (lispy--string-dwim))))) - (require (nth 0 handler)) (funcall (nth 1 handler) e-str)) (error "%s isn't supported currently" major-mode)))) +(defvar lispy-eval-expression-history nil) + (defun lispy-eval-expression () "Like `eval-expression', but for current language." (interactive) @@ -6904,7 +6802,8 @@ The result is a string." (if (member major-mode lispy-elisp-modes) #'lispy-mode #'ignore) - (read-from-minibuffer "Eval: ")))) + (read-from-minibuffer + "Eval: " nil nil nil 'lispy-eval-expression-history)))) (lispy-message (lispy--eval form)))) (defvar lispy-eval-match-data nil) @@ -7477,8 +7376,12 @@ See https://clojure.org/guides/weird_characters#_character_literal.") (lispy--read-1) ;; ——— ? char syntax —————————— (goto-char (point-min)) - (if (memq major-mode (cons 'hy-mode lispy-clojure-modes)) - (lispy--read-replace "[[:alnum:]-/*<>_?.,\\\\:!@#=]+" "clojure-symbol") + (cond + ((eq major-mode 'hy-mode) + (lispy--read-replace "[[:alnum:]-/*<>_?.,\\\\:!@#]+" "clojure-symbol")) + ((memq major-mode lispy-clojure-modes) + (lispy--read-replace "[[:alnum:]-+/*<>_?.\\\\:!@#=]+" "clojure-symbol")) + (t (while (re-search-forward "\\(?:\\s-\\|\\s(\\)\\?" nil t) (unless (lispy--in-string-or-comment-p) (let ((pt (point)) @@ -7486,12 +7389,18 @@ See https://clojure.org/guides/weird_characters#_character_literal.") (lispy--skip-elisp-char) (setq sexp (buffer-substring-no-properties pt (point))) (delete-region (1- pt) (point)) - (insert (format "(ly-raw char %S)" sexp)))))) + (insert (format "(ly-raw char %S)" sexp))))))) (when (eq major-mode 'clojure-mode) (lispy--read-replace " *,+" "clojure-commas")) ;; ——— \ char syntax (LISP)———— (goto-char (point-min)) - (while (re-search-forward "#\\\\\\(.\\)" nil t) + (while (let ((case-fold-search t)) + ;; http://lispworks.com/documentation/HyperSpec/Body/02_ac.htm + (re-search-forward (rx "#\\" (or "tab" "page" "space" + "return" "rubout" + "newline" "linefeed" + "backspace" any)) + nil t)) (unless (lispy--in-string-or-comment-p) (replace-match (format "(ly-raw lisp-char %S)" (substring-no-properties @@ -7939,7 +7848,8 @@ Defaults to `error'." (forward-line 1)) (move-marker end nil))) -(defvar lispy-no-indent-modes '(minibuffer-inactive-mode +(defvar lispy-no-indent-modes '(minibuffer-mode + minibuffer-inactive-mode comint-mode) "List of major modes where `indent-for-tab-command' should not be used. `lispy--indent-for-tab' will do nothing if the current mode or any of its parent @@ -8099,6 +8009,7 @@ Try to refresh if nil is returned." (defvar helm-update-blacklist-regexps) (defvar helm-candidate-number-limit) +(declare-function helm "ext:helm") (defvar lispy-tag-history nil "History for tags.") @@ -8148,9 +8059,7 @@ ACTION is called for the selected candidate." (defun lispy--action-jump (tag) "Jump to TAG." (if (eq (length tag) 3) - (with-selected-window (if (eq lispy-completion-method 'ivy) - (ivy--get-window ivy-last) - (selected-window)) + (progn (push-mark) (find-file (cadr tag)) (goto-char @@ -8257,7 +8166,7 @@ The outer delimiters are stripped." (delete-region beg (point)) (insert (cl-caddr sxp))) ((clojure-commas) - (delete-region (1- beg) (point)) + (delete-region (if (> beg (point-min)) (1- beg) beg) (point)) (insert (cl-caddr sxp))) (clojure-symbol (delete-region beg (point)) @@ -8797,98 +8706,105 @@ Return an appropriate `setq' expression when in `let', `dolist', (when tsexp (lispy-different) (cond - ((looking-back "(\\(?:lexical-\\)?let\\(?:\\*\\|-when-compile\\)?[ \t\n]*" - (line-beginning-position 0)) - (cons 'setq - (cl-mapcan - (lambda (x) (unless (listp x) (list x nil))) - tsexp))) - - ((lispy-after-string-p "(dolist ") - `(lispy--dolist-item-expr ',tsexp)) - - ((and (consp tsexp) - (eq (car tsexp) 'lambda) - (eq (length (cadr tsexp)) 1) - (looking-back "(map\\sw* +" - (line-beginning-position))) - `(lispy--mapcar-item-expr ,tsexp - ,(save-excursion - (lispy-different) - (read (current-buffer))))) - ((and (consp tsexp) - (eq (car tsexp) 'find-file-noselect)) - `(switch-to-buffer ,tsexp)) - - ;; point moves - ((progn - (lispy--out-backward 1) - (looking-back - "(\\(?:lexical-\\|if-\\|when-\\)?let\\(?:\\*\\|-when-compile\\)?[ \t\n]*" - (line-beginning-position 0))) - (cons - (if (eq major-mode 'scheme-mode) - 'define - 'setq) - tsexp)) - - ((looking-back - "(\\(?:cl-\\)?labels[ \t\n]*" - (line-beginning-position 0)) - (cons 'defun tsexp)) - - ((looking-at - "(cond\\b") - (let ((re tsexp)) - (if (cdr re) - `(if ,(car re) - (progn - ,@(cdr re)) - lispy--eval-cond-msg) - `(or ,(car re) - lispy--eval-cond-msg)))) - ((looking-at "(pcase\\s-*") - (goto-char (match-end 0)) - (if (eval (pcase--expand (lispy--read (lispy--string-dwim)) - `((,(car tsexp) t)))) - `(progn - ,(funcall (lispy--pcase-pattern-matcher (car tsexp)) - (eval (read (lispy--string-dwim)))) - "pcase: t") - "pcase: nil")) - ((looking-at "(cl-destructuring-bind") - (let* ((x-expr (read (lispy--string-dwim))) - (x-parts (eval (nth 2 x-expr)))) - (cl-mapc - #'set (nth 1 x-expr) x-parts) - (cons 'list (nth 1 x-expr)))) - ((looking-at "(with-current-buffer") - `(switch-to-buffer ,tsexp)) - ((and (looking-at "(\\(?:cl-\\)?\\(?:defun\\|defmacro\\)") - (save-excursion - (lispy-flow 1) - (eq (point) origin))) - (let* ((fn-name (save-excursion - (forward-char) - (forward-sexp 2) - (lispy--preceding-sexp))) - (int-form - (and (fboundp fn-name) - (interactive-form fn-name))) - (int-form (when (eq (car int-form) 'interactive) - (cond ((listp (cadr int-form)) - (cadr int-form)) - ((equal (cadr int-form) "p") - ''(1)))))) - (if int-form - `(lispy-destructuring-setq ,tsexp - ,int-form) - `(progn - ,@(mapcar - (lambda (x) - (list 'setq x nil)) - (delq '&key (delq '&optional (delq '&rest tsexp)))))))) - (t tsexp)))))) + ((looking-back "(\\(?:lexical-\\)?let\\(?:\\*\\|-when-compile\\)?[ \t\n]*" + (line-beginning-position 0)) + (cons 'setq + (cl-mapcan + (lambda (x) (unless (listp x) (list x nil))) + tsexp))) + + ((lispy-after-string-p "(dolist ") + `(lispy--dolist-item-expr ',tsexp)) + + ((lispy-after-string-p "(cl-destructuring-bind ") + (let* ((x-expr (read (lispy--string-dwim))) + (x-parts (eval (save-excursion + (lispy-down 1) + (read (lispy--string-dwim)))))) + (cl-mapc + #'set x-expr x-parts) + (cons 'list + (apply #'append + (mapcar (lambda (sym) + (list (list 'quote sym) (eval sym))) x-expr))))) + + ((and (consp tsexp) + (eq (car tsexp) 'lambda) + (eq (length (cadr tsexp)) 1) + (looking-back "(map\\sw* +" + (line-beginning-position))) + `(lispy--mapcar-item-expr ,tsexp + ,(save-excursion + (lispy-different) + (read (current-buffer))))) + ((and (consp tsexp) + (eq (car tsexp) 'find-file-noselect)) + `(switch-to-buffer ,tsexp)) + + ;; point moves + ((progn + (lispy--out-backward 1) + (looking-back + "(\\(?:lexical-\\|if-\\|when-\\)?let\\(?:\\*\\|-when-compile\\)?[ \t\n]*" + (line-beginning-position 0))) + (cons + (if (eq major-mode 'scheme-mode) + 'define + 'setq) + tsexp)) + + ((looking-back + "(\\(?:cl-\\)?labels[ \t\n]*" + (line-beginning-position 0)) + (cons 'defun tsexp)) + + ((looking-at + "(cond\\b") + (let ((re tsexp)) + (if (cdr re) + `(if ,(car re) + (progn + ,@(cdr re)) + lispy--eval-cond-msg) + `(or ,(car re) + lispy--eval-cond-msg)))) + ((looking-at "(pcase\\s-*") + (goto-char (match-end 0)) + (if (eval (pcase--expand (read (lispy--string-dwim)) + `((,(car tsexp) t)))) + (let ((pexpr (funcall (lispy--pcase-pattern-matcher (car tsexp)) + (eval (read (lispy--string-dwim)))))) + + `(progn + ,pexpr + ',pexpr)) + lispy--eval-pcase-msg)) + + ((and (looking-at "(\\(?:cl-\\)?\\(?:defun\\|defmacro\\)") + (save-excursion + (lispy-flow 1) + (eq (point) origin))) + (let* ((fn-name (save-excursion + (forward-char) + (forward-sexp 2) + (lispy--preceding-sexp))) + (int-form + (and (fboundp fn-name) + (interactive-form fn-name))) + (int-form (when (eq (car int-form) 'interactive) + (cond ((listp (cadr int-form)) + (cadr int-form)) + ((equal (cadr int-form) "p") + ''(1)))))) + (if int-form + `(lispy-destructuring-setq ,tsexp + ,int-form) + `(progn + ,@(mapcar + (lambda (x) + (list 'setq x nil)) + (delq '&key (delq '&optional (delq '&rest tsexp)))))))) + (t tsexp)))))) (defun lispy--find-unmatched-delimiters (beg end) "Return the positions of unmatched delimiters between BEG and END. @@ -9159,6 +9075,8 @@ FUNC is obtained from (`lispy--insert-or-call' DEF PLIST)." ("k" lispy-knight-up) ("z" nil)) +(autoload 'lispy-occur "lispy-occur") + (defvar lispy-mode-map-special (let ((map (make-sparse-keymap))) ;; navigation @@ -9201,8 +9119,8 @@ FUNC is obtained from (`lispy--insert-or-call' DEF PLIST)." ;; dialect-specific (lispy-define-key map "e" 'lispy-eval) (lispy-define-key map "E" 'lispy-eval-and-insert) - (lispy-define-key map "G" 'lispy-goto-local) - (lispy-define-key map "g" 'lispy-goto) + (lispy-define-key map "g" 'lispy-goto-local) + (lispy-define-key map "G" 'lispy-goto) (lispy-define-key map "F" 'lispy-follow t) (lispy-define-key map "D" 'pop-tag-mark) (lispy-define-key map "A" 'lispy-beginning-of-defun) @@ -9474,7 +9392,7 @@ When ARG is non-nil, unquote the current string." (insert "\"")) (t - (lispy--space-unless "^\\|\\s-\\|\\s(\\|[#]") + (lispy--space-unless "^\\|\\s-\\|\\s(\\|[#][a-zA-Z]?") (insert "\"\"") (unless (looking-at "\n\\|)\\|}\\|\\]\\|$") (just-one-space) diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..b05a279a --- /dev/null +++ b/mypy.ini @@ -0,0 +1,9 @@ +[mypy] +# disallow_untyped_defs = True +color_output = False + +[mypy-jedi.*] +ignore_missing_imports = True + +[mypy-__builtin__] +ignore_missing_imports = True \ No newline at end of file diff --git a/targets/install-deps.el b/targets/install-deps.el index f69ab770..158b8e3c 100644 --- a/targets/install-deps.el +++ b/targets/install-deps.el @@ -8,7 +8,7 @@ sly geiser clojure-mode - counsel + swiper hydra ace-window helm diff --git a/test/test_lispy-python.py b/test/test_lispy-python.py new file mode 100644 index 00000000..75677117 --- /dev/null +++ b/test/test_lispy-python.py @@ -0,0 +1,226 @@ +import io +import os +import re +import ast +import sys +from importlib.machinery import SourceFileLoader +from contextlib import redirect_stdout +from textwrap import dedent + +lp = SourceFileLoader("lispy-python", os.path.dirname(__file__) + "/../lispy-python.py").load_module() + +def print_elisp_to_str(obj): + with io.StringIO() as buf, redirect_stdout(buf): + lp.print_elisp(obj) + return buf.getvalue().strip() + + +def with_output_to_string(code): + with io.StringIO() as buf, redirect_stdout(buf): + exec(code) + return buf.getvalue().strip() + +def lp_eval(code, env={}): + return with_output_to_string(f"__res__=lp.eval_code('''{code}''', {env})") + +def test_exec(): + # Need to run this with globals(). + # Otherwise, x will not be defined later + exec("x=1", globals()) + assert x == 1 + +def test_tr_returns_1(): + code_1 = dedent(""" + x=1 + y=2 + return x + y""") + parsed = ast.parse(code_1).body + assert len(parsed) == 3 + translated = lp.tr_returns(parsed) + assert len(translated) == 3 + assert parsed[0] == translated[0] + assert parsed[1] == translated[1] + assert ast.unparse(translated[2]) == "return locals() | {'__return__': x + y}" + code = ast.unparse(lp.wrap_return(translated)) + exec(code, globals()) + assert x == 1 + assert y == 2 + assert __return__ == 3 + +def test_tr_returns_2(): + code_2 = dedent(""" + if os.environ.get("FOO"): + return 0 + x = 1 + y = 2 + return x + y""") + parsed = ast.parse(code_2).body + assert len(parsed) == 4 + translated = lp.tr_returns(parsed) + assert len(translated) == 4 + assert ast.unparse(translated[3]) == "return locals() | {'__return__': x + y}" + assert ast.unparse(translated[0].body) == "return locals() | {'__return__': 0}" + code = ast.unparse(lp.wrap_return(translated)) + exec(code, globals()) + assert x == 1 + assert y == 2 + assert __return__ == 3 + os.environ["FOO"] = "BAR" + exec(code, globals()) + assert __return__ == 0 + +def test_translate_def(): + code = dedent(""" + def add(x, y): + return x + y + """) + tr = lp.translate(code) + assert len(tr) == 1 + assert isinstance(tr[0], ast.FunctionDef) + r = lp.eval_code(code) + assert r["res"] == "'unset'" + assert "" \\)', r)