diff --git a/README.rst b/README.rst index da7b60a66..c8ec48160 100644 --- a/README.rst +++ b/README.rst @@ -35,6 +35,7 @@ Features - `Virtualenv support`_ - `On-the-fly syntax checking`_ - `Access to documentation`_ +- `Variable explorer`_ - `Debugging`_ - `Testing`_ - `Profiling`_ @@ -46,6 +47,7 @@ Features .. _On-the-fly syntax checking: https://elpy.readthedocs.io/en/latest/ide.html#syntax-checking .. _Interactive Python shell: https://elpy.readthedocs.io/en/latest/ide.html#interactive-python .. _Access to documentation: https://elpy.readthedocs.io/en/latest/ide.html#documentation +.. _Variable explorer: https://elpy.readthedocs.io/en/latest/ide.html#variable-explorer .. _Debugging: https://elpy.readthedocs.io/en/latest/ide.html#debugging .. _Testing: https://elpy.readthedocs.io/en/latest/ide.html#testing .. _Profiling: https://elpy.readthedocs.io/en/latest/ide.html#profiling diff --git a/docs/ide.rst b/docs/ide.rst index ef0d3a218..1ed7ddedc 100644 --- a/docs/ide.rst +++ b/docs/ide.rst @@ -552,6 +552,27 @@ currently selected company candidate. The idle delay in seconds until documentation is updated automatically. +Variable explorer +================= + +Elpy offers a way of visualizing the variables defined in the current python interpreter. + +.. command:: elpy-ve-display-variable-explorer + :kbd: C-c C-a + + Display a buffer with a list of the variables currently defined and + their values. You can navigate this list with the `up` and `down` + arrows. For convenience, variables with long names or values (like + long list or arrays) are truncated. Hitting `return` on a given + line will display a new buffer with the full variable name and + value. The variable explorer is automatically refreshed, but you can + refresh it manually by hitting `C-c C-a` again. + +.. option:: elpy-ve-row-max-height + + Maximum height of a variable explorer row (default to 10 characters). + + Snippets ======== diff --git a/elpy-shell.el b/elpy-shell.el index 1c203e0df..7c459ce2f 100644 --- a/elpy-shell.el +++ b/elpy-shell.el @@ -1211,6 +1211,291 @@ region or buffer." (remove-overlays (point-min) (point-max) 'elpy-breakpoint t)))) +;;;;;;;;;;;;;;;;;;;;;;; +;; Variable explorer + +(defcustom elpy-ve-row-max-height 10 + "Maximum height of a variable explorer row." + :group 'elpy + :type 'number) + +(defvar elpy-ve-get-variables-code + "def __elpy_get_variables(): + import json + try: + import numpy as np + NUMPY = True + except: + NUMPY = False + import pprint + try: + # python2 + from StringIO import StringIO + except ImportError: + # python3 + from io import StringIO + locs = globals().copy() + ret = {} + for var, val in locs.items(): + typ = None + tmp_str = StringIO() + # filter out builtins, module and functions + if var == '_' or '__' in var or ' (length string) column-content-width) + (concat + (substring string 0 (- column-content-width 3)) + "...") + string))) + +(defun elpy-ve--insert-separator (width) + "Insert a horizontal separator of width WIDTH." + (concat + (propertize (s-repeat width "-") + 'face 'font-lock-comment-face) + " +")) + +(defun elpy-ve--insert-varname (varname varname-long width) + "Insert a variable name. + +VARNAME-LONG is the variable name while +VARNAME is the variable name trimmed to be WIDTH long." + (concat + " " + (propertize varname + 'face 'font-lock-keyword-face + 'elpy-ve-variable varname-long) + (s-repeat (- width (length varname)) " "))) + +(defun elpy-ve--insert-vartype (vartype width) + "Insert the variable type VARTYPE. + +Ensure that it fills WIDTH." + (concat + (propertize vartype 'face 'font-lock-variable-name-face) + (s-repeat (- width (length vartype)) " "))) + +(defun elpy-ve--display-a-variable (var width) + "Display the variable VAR using columns of width WIDTH." + (let* ((varname-long (string-trim (symbol-name (car var)))) + (varname (elpy-ve--truncate-string varname-long width)) + (varval (string-trim (car (cdr var)))) + (vartype-long (string-trim (car (cdr (cdr var))))) + (vartype (elpy-ve--truncate-string vartype-long width))) + (insert + (concat + ;; separator + (elpy-ve--insert-separator (* width 4)) + ;; variable name + (elpy-ve--insert-varname varname varname-long width) + ;; variable type + (elpy-ve--insert-vartype vartype width))) + ;; variable value + (let* ((first t) + (all-lines (split-string varval "\n")) + (cropped (> (length all-lines) elpy-ve-row-max-height)) + (lines (if cropped + (reverse (nthcdr (- (length all-lines) elpy-ve-row-max-height) + (reverse all-lines))) + all-lines))) + (dolist (line lines) + (let* ((line (elpy-ve--truncate-string line + (* 2 width)))) + (insert + (concat + (if (not first) + (s-repeat (+ 1 (* width 2)) " ") + (setq first nil) + "") + line + " +")))) + ;; if cropped, add an indicator + (when cropped + (insert + (concat + (s-repeat (+ 1 (* width 2)) " ") + "... +") + ))))) + +(defun elpy-ve--display-a-detailled-variable (var) + "Display the variable VAR in a detailled view." + (let* ((varname (symbol-name (car var))) + (varval (car (cdr var))) + (vartype (car (cdr (cdr var))))) + (insert + (concat + ;; separator + (elpy-ve--insert-separator 80) + ;; variable name + (elpy-ve--insert-varname varname nil (length varname)) + " " + ;; variable type + (elpy-ve--insert-vartype vartype (length vartype)) + " +" + ;; separator + (elpy-ve--insert-separator 80))) + ;; variable value + (dolist (line (split-string varval "\n")) + (insert + (concat + " " + line + " +" + ))))) + +(defun elpy-ve-display-variable-explorer () + "Display the variable explorer." + (interactive) + ;; check if shell is started + (unless (get-buffer-process (format "*%s*" + (python-shell-get-process-name nil))) + (error "No shell runnning")) + ;; check if the shell is available + (when (not (elpy-shell--check-if-shell-available)) + (error "Shell is not currently available")) + (let ((vars (elpy-ve-get-variables-from-shell))) + (when (= (length vars) 0) + (error "No variables found in the current shell")) + (switch-to-buffer-other-window + (get-buffer-create "*Elpy Variable Explorer*")) + (let ((inhibit-read-only t) + (width (/ (window-width) 4))) + (erase-buffer) + (dolist (var vars) (elpy-ve--display-a-variable var width)) + (insert (elpy-ve--insert-separator (* width 4))) + (elpy-ve-mode) + (goto-char (point-min)) + (elpy-ve-goto-next-entry)))) + +(defun elpy-ve-goto-next-entry () + "Go to the next entry of the variable explorer." + (interactive) + (let ((current-pos (point))) + (if (re-search-forward "^ [^- ]" nil t) + (forward-char -1) + (goto-char current-pos) + (error "No next entry")))) + +(defun elpy-ve-goto-prev-entry () + "Go to the previous entry of the variable explorer." + (interactive) + (let ((current-pos (point))) + (if (re-search-backward "^ [^- ]" nil t) + (forward-char 1) + (goto-char current-pos) + (error "No previous entry")))) + +(defun elpy-ve-display-variable-at-point () + "Display a detailled view of the variable at point in another buffer." + (interactive) + (beginning-of-line) + (forward-char 1) + (when (not (get-text-property (point) 'elpy-ve-variable)) + (elpy-ve-goto-prev-entry)) + (let* ((varname (intern (get-text-property (point) 'elpy-ve-variable))) + (var (cdr (assoc varname elpy-ve--variables-cache))) + (bufname "*Elpy Variable Explorer[detail]*")) + (let ((pop-up-windows t)) + (display-buffer (get-buffer-create bufname))) + (with-current-buffer bufname + (let ((inhibit-read-only t)) + (erase-buffer) + (elpy-ve--display-a-detailled-variable (cons varname var)) + (elpy-ve-detail-mode) + (goto-char (point-min)) + (forward-line 1))))) + +(defun elpy-ve-quit-all-windows () + "Close all variable explorer buffers." + (interactive) + (cl-loop for buffer being the buffers do + (when (and (buffer-name buffer) + (string-match "\\*Elpy Variable Explorer.*\\*" + (buffer-name buffer))) + (with-current-buffer buffer + (if (get-buffer-window buffer) + (quit-restore-window) + (kill-buffer buffer)))))) + +(defvar elpy-ve-mode-map + (let ((map (make-sparse-keymap))) + (define-key map [remap next-line] 'elpy-ve-goto-next-entry) + (define-key map [remap previous-line] 'elpy-ve-goto-prev-entry) + (define-key map (kbd "q") 'elpy-ve-quit-all-windows) + (define-key map (kbd "RET") 'elpy-ve-display-variable-at-point) + map) + "Keymap for `elpy-ve-mode'.") + +(define-derived-mode elpy-ve-mode fundamental-mode "Elpy variable explorer" + "Major mode for displaying Elpy's variable explorer. + +\\{elpy-ve-mode-map}" + (read-only-mode)) + +(defvar elpy-ve-detail-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "q") 'quit-window) + map) + "Keymap for `elpy-ve-detail-mode'.") + +(define-derived-mode elpy-ve-detail-mode fundamental-mode "Elpy detailed variable display" + "Major mode for displaying Elpy's detailled variables. + +\\{elpy-ve-detail-mode-map}" + (read-only-mode)) + ;;;;;;;;;;;;;;;;;;;;;;; ;; Deprecated functions diff --git a/elpy.el b/elpy.el index 845385a53..ced90c985 100644 --- a/elpy.el +++ b/elpy.el @@ -417,6 +417,7 @@ option is `pdb'." (define-key map (kbd "C-c C-z") 'elpy-shell-switch-to-shell) (define-key map (kbd "C-c C-k") 'elpy-shell-kill) (define-key map (kbd "C-c C-K") 'elpy-shell-kill-all) + (define-key map (kbd "C-c C-a") 'elpy-ve-display-variable-explorer) (define-key map (kbd "C-c C-r") elpy-refactor-map) (define-key map (kbd "C-c C-x") elpy-django-mode-map) @@ -539,6 +540,8 @@ This option need to bet set through `customize' or `customize-set-variable' to b :help "Send the current region or the whole buffer to Python"] ["Send Definition" python-shell-send-defun :help "Send current definition to Python"] + ["Variable explorer" elpy-ve-display-variable-explorer + :help "Display the variable explorer"] ["Kill Python shell" elpy-shell-kill :help "Kill the current Python shell"] ["Kill all Python shells" elpy-shell-kill-all diff --git a/test/elpy-ve--display-a-detailled-variable-test.el b/test/elpy-ve--display-a-detailled-variable-test.el new file mode 100644 index 000000000..9dfe55283 --- /dev/null +++ b/test/elpy-ve--display-a-detailled-variable-test.el @@ -0,0 +1,10 @@ + +(ert-deftest elpy-ve--display-a-detailled-variable-should-display-properly () + (elpy-testcase () + (let ((var '(c "['this', 'is', 'a', 'list']" "list[4]"))) + (with-temp-buffer + (elpy-ve--display-a-variable var 20) + (should (string= (substring-no-properties (buffer-string)) + "-------------------------------------------------------------------------------- + c list[4] ['this', 'is', 'a', 'list'] +")))))) diff --git a/test/elpy-ve--display-a-variable-test.el b/test/elpy-ve--display-a-variable-test.el new file mode 100644 index 000000000..824beeb9b --- /dev/null +++ b/test/elpy-ve--display-a-variable-test.el @@ -0,0 +1,45 @@ + +(ert-deftest elpy-ve--display-a-variable-should-display-a-variable () + (elpy-testcase () + (let ((var '(c "['this', 'is', 'a', 'list']" "list[4]"))) + (with-temp-buffer + (elpy-ve--display-a-variable var 20) + (should (string= (substring-no-properties (buffer-string)) + "-------------------------------------------------------------------------------- + c list[4] ['this', 'is', 'a', 'list'] +")))))) + +(ert-deftest elpy-ve--display-a-variable-should-crop-horizontally () + (elpy-testcase () + (let ((var '(c "['this', 'is', 'a', 'list']" "list[4]"))) + (with-temp-buffer + (elpy-ve--display-a-variable var 10) + (should (string= (substring-no-properties (buffer-string)) + "---------------------------------------- + c list[4] ['this', 'is', ... +")))))) + +(ert-deftest elpy-ve--display-a-variable-should-crop-vertically () + (elpy-testcase () + (let ((var '(c "[['this', 'is', 'a', 'list'], + ['this', 'is', 'a', 'list'], + ['this', 'is', 'a', 'list'], + ['this', 'is', 'a', 'list'], + ['this', 'is', 'a', 'list'], + ['this', 'is', 'a', 'list'], + ['this', 'is', 'a', 'list'], + ['this', 'is', 'a', 'list'], + ['this', 'is', 'a', 'list'], + ['this', 'is', 'a', 'list']]" "list[10]"))) + (with-temp-buffer + (let ((elpy-ve-row-max-height 5)) + (elpy-ve--display-a-variable var 20)) + (should (string= (substring-no-properties (buffer-string)) + "-------------------------------------------------------------------------------- + c list[10] [['this', 'is', 'a', 'list'], + ['this', 'is', 'a', 'list'], + ['this', 'is', 'a', 'list'], + ['this', 'is', 'a', 'list'], + ['this', 'is', 'a', 'list'], + ... +")))))) diff --git a/test/elpy-ve-display-variable-explorer-test.el b/test/elpy-ve-display-variable-explorer-test.el new file mode 100644 index 000000000..880019db3 --- /dev/null +++ b/test/elpy-ve-display-variable-explorer-test.el @@ -0,0 +1,76 @@ +(ert-deftest elpy-ve-display-variable-explorer-should-display () + (elpy-testcase () + (python-mode) + (elpy-mode) + (insert "a = 3\n") + (insert "b = \"this is a string\"\n") + (insert "c = ['this', 'is', 'a', 'list']\n") + (insert "print('OK')\n") + (elpy-shell-send-region-or-buffer) + (with-current-buffer "*Python*" + (elpy/wait-for-output "OK")) + (while (not (elpy-shell--check-if-shell-available)) + (sleep-for 0.1)) + (mletf* ((window-width () 100)) + (elpy-ve-display-variable-explorer)) + (should (string= (substring-no-properties (buffer-string)) + "---------------------------------------------------------------------------------------------------- + a int 3 +---------------------------------------------------------------------------------------------------- + b str[16] \"this is a string\" +---------------------------------------------------------------------------------------------------- + c list[4] ['this', 'is', 'a', 'list'] +---------------------------------------------------------------------------------------------------- +")))) + +(ert-deftest elpy-ve-display-variable-explorer-should-display-detailed-var () + (elpy-testcase () + (python-mode) + (elpy-mode) + (insert "a = 3\n") + (insert "b = \"this is a string\"\n") + (insert "c = ['this', 'is', 'a', 'list']\n") + (insert "print('OK')\n") + (elpy-shell-send-region-or-buffer) + (with-current-buffer "*Python*" + (elpy/wait-for-output "OK")) + (while (not (elpy-shell--check-if-shell-available)) + (sleep-for 0.1)) + (mletf* ((window-width () 40)) + (elpy-ve-display-variable-explorer) + (elpy-ve-display-variable-at-point)) + (other-window 1) + (should (search-forward "a int")) + (should (search-forward "3")))) + +(ert-deftest elpy-ve-goto-next-entry-should-work () + (elpy-testcase () + (python-mode) + (elpy-mode) + (insert "a = 3\n") + (insert "b = \"this is a string\"\n") + (insert "c = ['this', 'is', 'a', 'list']\n") + (insert "print('OK')\n") + (elpy-shell-send-region-or-buffer) + (with-current-buffer "*Python*" + (elpy/wait-for-output "OK")) + (while (not (elpy-shell--check-if-shell-available)) + (sleep-for 0.1)) + (mletf* ((window-width () 40)) + (elpy-ve-display-variable-explorer)) + (elpy-ve-goto-next-entry) + (should (string-match "str\\[16\\] *\"this is" + (buffer-substring-no-properties + (line-beginning-position) + (line-end-position)))) + (elpy-ve-goto-next-entry) + (should (string-match "list\\[4\\] *\\['this'," + (buffer-substring-no-properties + (line-beginning-position) + (line-end-position)))) + (elpy-ve-goto-prev-entry) + (should (string-match "str\\[16\\] *\"this is" + (buffer-substring-no-properties + (line-beginning-position) + (line-end-position)))) + )) diff --git a/test/elpy-ve-variables-from-shell-test.el b/test/elpy-ve-variables-from-shell-test.el new file mode 100644 index 000000000..fccd8f4ef --- /dev/null +++ b/test/elpy-ve-variables-from-shell-test.el @@ -0,0 +1,39 @@ + +(ert-deftest elpy-ve-variables-from-shell-should-return-variables () + (elpy-testcase () + (python-mode) + (elpy-mode) + (insert "a = 3\n") + (insert "b = \"this is a string\"\n") + (insert "c = ['this', 'is', 'a', 'list']\n") + (insert "print('OK')\n") + (elpy-shell-send-region-or-buffer) + (with-current-buffer "*Python*" + (elpy/wait-for-output "OK")) + (while (not (elpy-shell--check-if-shell-available)) + (sleep-for 0.1)) + (should (equal (elpy-ve-get-variables-from-shell) + '((a "3 +" "int") (b "\"this is a string\"" "str[16]") (c "['this', 'is', 'a', 'list'] +" "list[4]")))))) + +(ert-deftest elpy-ve-variables-from-shell-should-ignore-function-and-modules () + (elpy-testcase () + (python-mode) + (elpy-mode) + (insert "import sys\n") + (insert "def foo(a, b):\n") + (insert " return a + b\n") + (insert "a = 3\n") + (insert "b = \"this is a string\"\n") + (insert "c = ['this', 'is', 'a', 'list']\n") + (insert "print('OK')\n") + (elpy-shell-send-region-or-buffer) + (with-current-buffer "*Python*" + (elpy/wait-for-output "OK")) + (while (not (elpy-shell--check-if-shell-available)) + (sleep-for 0.1)) + (should (equal (elpy-ve-get-variables-from-shell) + '((a "3 +" "int") (b "\"this is a string\"" "str[16]") (c "['this', 'is', 'a', 'list'] +" "list[4]"))))))