diff options
Diffstat (limited to 'lisp/progmodes/python.el')
-rw-r--r-- | lisp/progmodes/python.el | 403 |
1 files changed, 338 insertions, 65 deletions
diff --git a/lisp/progmodes/python.el b/lisp/progmodes/python.el index 266d193cdda..2165c835057 100644 --- a/lisp/progmodes/python.el +++ b/lisp/progmodes/python.el @@ -177,12 +177,14 @@ ;; might guessed you should run `python-shell-send-buffer' from time ;; to time to get better results too. -;; Imenu: This mode supports Imenu in its most basic form, letting it -;; build the necessary alist via `imenu-default-create-index-function' -;; by having set `imenu-extract-index-name-function' to -;; `python-info-current-defun' and -;; `imenu-prev-index-position-function' to -;; `python-imenu-prev-index-position'. +;; Imenu: There are two index building functions to be used as +;; `imenu-create-index-function': `python-imenu-create-index' (the +;; default one, builds the alist in form of a tree) and +;; `python-imenu-create-flat-index'. See also +;; `python-imenu-format-item-label-function', +;; `python-imenu-format-parent-item-label-function', +;; `python-imenu-format-parent-item-jump-label-function' variables for +;; changing the way labels are formatted in the tree version. ;; If you used python-mode.el you probably will miss auto-indentation ;; when inserting newlines. To achieve the same behavior you have @@ -213,12 +215,10 @@ (require 'ansi-color) (require 'comint) -(eval-when-compile - (require 'cl) - ;; Avoid compiler warnings - (defvar view-return-to-alist) - (defvar compilation-error-regexp-alist) - (defvar outline-heading-end-regexp)) +;; Avoid compiler warnings +(defvar view-return-to-alist) +(defvar compilation-error-regexp-alist) +(defvar outline-heading-end-regexp) (autoload 'comint-mode "comint") @@ -370,16 +370,30 @@ This variant of `rx' supports common python named REGEXPS." ;;; Font-lock and syntax +(eval-when-compile + (defun python-syntax--context-compiler-macro (form type &optional syntax-ppss) + (pcase type + (`'comment + `(let ((ppss (or ,syntax-ppss (syntax-ppss)))) + (and (nth 4 ppss) (nth 8 ppss)))) + (`'string + `(let ((ppss (or ,syntax-ppss (syntax-ppss)))) + (and (nth 3 ppss) (nth 8 ppss)))) + (`'paren + `(nth 1 (or ,syntax-ppss (syntax-ppss)))) + (_ form)))) + (defun python-syntax-context (type &optional syntax-ppss) "Return non-nil if point is on TYPE using SYNTAX-PPSS. TYPE can be `comment', `string' or `paren'. It returns the start character address of the specified TYPE." + (declare (compiler-macro python-syntax--context-compiler-macro)) (let ((ppss (or syntax-ppss (syntax-ppss)))) - (case type - (comment (and (nth 4 ppss) (nth 8 ppss))) - (string (and (not (nth 4 ppss)) (nth 8 ppss))) - (paren (nth 1 ppss)) - (t nil)))) + (pcase type + (`comment (and (nth 4 ppss) (nth 8 ppss))) + (`string (and (nth 3 ppss) (nth 8 ppss))) + (`paren (nth 1 ppss)) + (_ nil)))) (defun python-syntax-context-type (&optional syntax-ppss) "Return the context type using SYNTAX-PPSS. @@ -491,8 +505,8 @@ The type returned can be `comment', `string' or `paren'." (when (re-search-forward re limit t) (while (and (python-syntax-context 'paren) (re-search-forward re limit t))) - (if (and (not (python-syntax-context 'paren)) - (not (equal (char-after (point-marker)) ?=))) + (if (not (or (python-syntax-context 'paren) + (equal (char-after (point-marker)) ?=))) t (set-match-data nil))))) (1 font-lock-variable-name-face nil nil)) @@ -526,7 +540,7 @@ is used to limit the scan." (while (and (< i 3) (or (not limit) (< (+ point i) limit)) (eq (char-after (+ point i)) quote-char)) - (incf i)) + (setq i (1+ i))) i)) (defun python-syntax-stringify () @@ -744,17 +758,17 @@ START is the buffer position where the sexp starts." (save-restriction (widen) (save-excursion - (case context-status - ('no-indent 0) + (pcase context-status + (`no-indent 0) ;; When point is after beginning of block just add one level ;; of indentation relative to the context-start - ('after-beginning-of-block + (`after-beginning-of-block (goto-char context-start) (+ (current-indentation) python-indent-offset)) ;; When after a simple line just use previous line ;; indentation, in the case current line starts with a ;; `python-indent-dedenters' de-indent one level. - ('after-line + (`after-line (- (save-excursion (goto-char context-start) @@ -771,11 +785,11 @@ START is the buffer position where the sexp starts." ;; When inside of a string, do nothing. just use the current ;; indentation. XXX: perhaps it would be a good idea to ;; invoke standard text indentation here - ('inside-string + (`inside-string (goto-char context-start) (current-indentation)) ;; After backslash we have several possibilities. - ('after-backslash + (`after-backslash (cond ;; Check if current line is a dot continuation. For this ;; the current line must start with a dot and previous @@ -841,7 +855,7 @@ START is the buffer position where the sexp starts." (+ (current-indentation) python-indent-offset))))) ;; When inside a paren there's a need to handle nesting ;; correctly - ('inside-paren + (`inside-paren (cond ;; If current line closes the outermost open paren use the ;; current indentation of the context-start line. @@ -1180,6 +1194,70 @@ Returns nil if point is not in a def or class." ;; Ensure point moves forward. (and (> beg-pos (point)) (goto-char beg-pos))))) +(defun python-nav--syntactically (fn poscompfn &optional contextfn) + "Move point using FN avoiding places with specific context. +FN must take no arguments. POSCOMPFN is a two arguments function +used to compare current and previous point after it is moved +using FN, this is normally a less-than or greater-than +comparison. Optional argument CONTEXTFN defaults to +`python-syntax-context-type' and is used for checking current +point context, it must return a non-nil value if this point must +be skipped." + (let ((contextfn (or contextfn 'python-syntax-context-type)) + (start-pos (point-marker)) + (prev-pos)) + (catch 'found + (while t + (let* ((newpos + (and (funcall fn) (point-marker))) + (context (funcall contextfn))) + (cond ((and (not context) newpos + (or (and (not prev-pos) newpos) + (and prev-pos newpos + (funcall poscompfn newpos prev-pos)))) + (throw 'found (point-marker))) + ((and newpos context) + (setq prev-pos (point))) + (t (when (not newpos) (goto-char start-pos)) + (throw 'found nil)))))))) + +(defun python-nav--forward-defun (arg) + "Internal implementation of python-nav-{backward,forward}-defun. +Uses ARG to define which function to call, and how many times +repeat it." + (let ((found)) + (while (and (> arg 0) + (setq found + (python-nav--syntactically + (lambda () + (re-search-forward + python-nav-beginning-of-defun-regexp nil t)) + '>))) + (setq arg (1- arg))) + (while (and (< arg 0) + (setq found + (python-nav--syntactically + (lambda () + (re-search-backward + python-nav-beginning-of-defun-regexp nil t)) + '<))) + (setq arg (1+ arg))) + found)) + +(defun python-nav-backward-defun (&optional arg) + "Navigate to closer defun backward ARG times. +Unlikely `python-nav-beginning-of-defun' this doesn't care about +nested definitions." + (interactive "^p") + (python-nav--forward-defun (- (or arg 1)))) + +(defun python-nav-forward-defun (&optional arg) + "Navigate to closer defun forward ARG times. +Unlikely `python-nav-beginning-of-defun' this doesn't care about +nested definitions." + (interactive "^p") + (python-nav--forward-defun (or arg 1))) + (defun python-nav-beginning-of-statement () "Move to start of current statement." (interactive "^") @@ -2218,11 +2296,11 @@ INPUT." 'default) (t nil))) (completion-code - (case completion-context - (pdb python-shell-completion-pdb-string-code) - (import python-shell-completion-module-string-code) - (default python-shell-completion-string-code) - (t nil))) + (pcase completion-context + (`pdb python-shell-completion-pdb-string-code) + (`import python-shell-completion-module-string-code) + (`default python-shell-completion-string-code) + (_ nil))) (input (if (eq completion-context 'import) (replace-regexp-in-string "^[ \t]+" "" line) @@ -2335,7 +2413,7 @@ Argument OUTPUT is a string with the output from the comint process." ;; encounters an exception, it prints the _entire_ stack ;; trace. To handle all of these cases, we want to find ;; the _last_ stack frame printed in the most recent - ;; batch of output, then jump to the corrsponding + ;; batch of output, then jump to the corresponding ;; file/line number. (goto-char (point-max)) (when (re-search-backward python-pdbtrack-stacktrace-info-regexp nil t) @@ -2530,12 +2608,12 @@ JUSTIFY should be used (if applicable) as in `fill-paragraph'." JUSTIFY should be used (if applicable) as in `fill-paragraph'." (let* ((marker (point-marker)) (str-start-pos - (let ((m (make-marker))) - (setf (marker-position m) - (or (python-syntax-context 'string) - (and (equal (string-to-syntax "|") - (syntax-after (point))) - (point)))) m)) + (set-marker + (make-marker) + (or (python-syntax-context 'string) + (and (equal (string-to-syntax "|") + (syntax-after (point))) + (point))))) (num-quotes (python-syntax-count-quotes (char-after str-start-pos) str-start-pos)) (str-end-pos @@ -2548,17 +2626,17 @@ JUSTIFY should be used (if applicable) as in `fill-paragraph'." ;; Docstring styles may vary for oneliners and multi-liners. (> (count-matches "\n" str-start-pos str-end-pos) 0)) (delimiters-style - (case python-fill-docstring-style + (pcase python-fill-docstring-style ;; delimiters-style is a cons cell with the form ;; (START-NEWLINES . END-NEWLINES). When any of the sexps ;; is NIL means to not add any newlines for start or end ;; of docstring. See `python-fill-docstring-style' for a ;; graphic idea of each style. - (django (cons 1 1)) - (onetwo (and multi-line-p (cons 1 2))) - (pep-257 (and multi-line-p (cons nil 2))) - (pep-257-nn (and multi-line-p (cons nil 1))) - (symmetric (and multi-line-p (cons 1 1))))) + (`django (cons 1 1)) + (`onetwo (and multi-line-p (cons 1 2))) + (`pep-257 (and multi-line-p (cons nil 2))) + (`pep-257-nn (and multi-line-p (cons nil 1))) + (`symmetric (and multi-line-p (cons 1 1))))) (docstring-p (save-excursion ;; Consider docstrings those strings which ;; start on a line by themselves. @@ -2644,8 +2722,8 @@ the if condition." (defvar python-skeleton-available '() "Internal list of available skeletons.") -(define-abbrev-table 'python-mode-abbrev-table () - "Abbrev table for Python mode." +(define-abbrev-table 'python-mode-skeleton-abbrev-table () + "Abbrev table for Python mode skeletons." :case-fixed t ;; Allow / inside abbrevs. :regexp "\\(?:^\\|[^/]\\)\\<\\([[:word:]/]+\\)\\W*" @@ -2658,13 +2736,13 @@ the if condition." (defmacro python-skeleton-define (name doc &rest skel) "Define a `python-mode' skeleton using NAME DOC and SKEL. The skeleton will be bound to python-skeleton-NAME and will -be added to `python-mode-abbrev-table'." +be added to `python-mode-skeleton-abbrev-table'." (declare (indent 2)) (let* ((name (symbol-name name)) (function-name (intern (concat "python-skeleton-" name)))) `(progn - (define-abbrev python-mode-abbrev-table ,name "" ',function-name - :system t) + (define-abbrev python-mode-skeleton-abbrev-table + ,name "" ',function-name :system t) (setq python-skeleton-available (cons ',function-name python-skeleton-available)) (define-skeleton ,function-name @@ -2672,6 +2750,10 @@ be added to `python-mode-abbrev-table'." (format "Insert %s statement." name)) ,@skel)))) +(define-abbrev-table 'python-mode-abbrev-table () + "Abbrev table for Python mode." + :parents (list python-mode-skeleton-abbrev-table)) + (defmacro python-define-auxiliary-skeleton (name doc &optional &rest skel) "Define a `python-mode' auxiliary skeleton using NAME DOC and SKEL. The skeleton will be bound to python-skeleton-NAME." @@ -2759,7 +2841,7 @@ The skeleton will be bound to python-skeleton-NAME." (easy-menu-add-item nil '("Python" "Skeletons") `[,(format - "Insert %s" (caddr (split-string (symbol-name skeleton) "-"))) + "Insert %s" (nth 2 (split-string (symbol-name skeleton) "-"))) ,skeleton t])))) ;;; FFAP @@ -2928,15 +3010,193 @@ Interactively, prompt for symbol." ;;; Imenu -(defun python-imenu-prev-index-position () - "Python mode's `imenu-prev-index-position-function'." - (let ((found)) - (while (and (setq found - (re-search-backward python-nav-beginning-of-defun-regexp nil t)) - (not (python-info-looking-at-beginning-of-defun)))) - (and found - (python-info-looking-at-beginning-of-defun) - (python-info-current-defun)))) +(defvar python-imenu-format-item-label-function + 'python-imenu-format-item-label + "Imenu function used to format an item label. +It must be a function with two arguments: TYPE and NAME.") + +(defvar python-imenu-format-parent-item-label-function + 'python-imenu-format-parent-item-label + "Imenu function used to format a parent item label. +It must be a function with two arguments: TYPE and NAME.") + +(defvar python-imenu-format-parent-item-jump-label-function + 'python-imenu-format-parent-item-jump-label + "Imenu function used to format a parent jump item label. +It must be a function with two arguments: TYPE and NAME.") + +(defun python-imenu-format-item-label (type name) + "Return imenu label for single node using TYPE and NAME." + (format "%s (%s)" name type)) + +(defun python-imenu-format-parent-item-label (type name) + "Return imenu label for parent node using TYPE and NAME." + (format "%s..." (python-imenu-format-item-label type name))) + +(defun python-imenu-format-parent-item-jump-label (type name) + "Return imenu label for parent node jump using TYPE and NAME." + (if (string= type "class") + "*class definition*" + "*function definition*")) + +(defun python-imenu--put-parent (type name pos num-children tree &optional root) + "Add the parent with TYPE, NAME, POS and NUM-CHILDREN to TREE. +Optional Argument ROOT must be non-nil when the node being +processed is the root of the TREE." + (let ((label + (funcall python-imenu-format-item-label-function type name)) + (jump-label + (funcall python-imenu-format-parent-item-jump-label-function type name))) + (if root + ;; This is the root, everything is a children. + (cons label (cons (cons jump-label pos) tree)) + ;; This is node a which may contain some children. + (cons + (cons label (cons (cons jump-label pos) + ;; Append all the children + (python-util-popn tree num-children))) + ;; All previous non-children nodes. + (nthcdr num-children tree))))) + +(defun python-imenu--build-tree (&optional min-indent prev-indent num-children tree) + "Recursively build the tree of nested definitions of a node. +Arguments MIN-INDENT PREV-INDENT NUM-CHILDREN and TREE are +internal and should not be passed explicitly unless you know what +you are doing." + (setq num-children (or num-children 0) + min-indent (or min-indent 0)) + (let* ((pos (python-nav-backward-defun)) + (type) + (name (when (and pos (looking-at python-nav-beginning-of-defun-regexp)) + (let ((split (split-string (match-string-no-properties 0)))) + (setq type (car split)) + (cadr split)))) + (label (when name + (funcall python-imenu-format-item-label-function type name))) + (indent (current-indentation))) + (cond ((not pos) + ;; No defun found, nothing to add. + tree) + ((equal indent 0) + (if (> num-children 0) + ;; Append it as the parent of everything collected to + ;; this point. + (python-imenu--put-parent type name pos num-children tree t) + ;; There are no children, this is a lonely defun. + (cons label pos))) + ((equal min-indent indent) + ;; Stop collecting nodes after moving to a position with + ;; indentation equaling min-indent. This is specially + ;; useful for navigating nested definitions recursively. + tree) + (t + (python-imenu--build-tree + min-indent + indent + ;; Add another children, either when this is the + ;; first call or when indentation is + ;; less-or-equal than previous. And do not + ;; discard the number of children, because the + ;; way code is scanned, all children are + ;; collected until a root node yet to be found + ;; appears. + (if (or (not prev-indent) + (and + (> indent min-indent) + (<= indent prev-indent))) + (1+ num-children) + num-children) + (cond ((not prev-indent) + ;; First call to the function: append this + ;; defun to the index. + (list (cons label pos))) + ((= indent prev-indent) + ;; Add another defun with the same depth + ;; as the previous. + (cons (cons label pos) tree)) + ((and (< indent prev-indent) + (< 0 num-children)) + ;; There are children to be appended and + ;; the previous defun had more + ;; indentation, the current one must be a + ;; parent. + (python-imenu--put-parent type name pos num-children tree)) + ((> indent prev-indent) + ;; There are children defuns deeper than + ;; current depth. Fear not, we already + ;; know how to treat them. + (cons + (prog1 + (python-imenu--build-tree + prev-indent indent 1 (list (cons label pos))) + ;; Adjustment: after scanning backwards + ;; for all deeper children, we need to + ;; continue our scan for a parent from + ;; the current defun we are looking at. + (python-nav-forward-defun)) + tree)))))))) + +(defun python-imenu-create-index () + "Return tree Imenu alist for the current python buffer. +Change `python-imenu-format-item-label-function', +`python-imenu-format-parent-item-label-function', +`python-imenu-format-parent-item-jump-label-function' to +customize how labels are formatted." + (goto-char (point-max)) + (let ((index) + (tree)) + (while (setq tree (python-imenu--build-tree)) + (setq index (cons tree index))) + index)) + +(defun python-imenu-create-flat-index (&optional alist prefix) + "Return flat outline of the current python buffer for Imenu. +Optional Argument ALIST is the tree to be flattened, when nil +`python-imenu-build-index' is used with +`python-imenu-format-parent-item-jump-label-function' +`python-imenu-format-parent-item-label-function' +`python-imenu-format-item-label-function' set to (lambda (type +name) name). Optional Argument PREFIX is used in recursive calls +and should not be passed explicitly. + +Converts this: + + \((\"Foo\" . 103) + (\"Bar\" . 138) + (\"decorator\" + (\"decorator\" . 173) + (\"wrap\" + (\"wrap\" . 353) + (\"wrapped_f\" . 393)))) + +To this: + + \((\"Foo\" . 103) + (\"Bar\" . 138) + (\"decorator\" . 173) + (\"decorator.wrap\" . 353) + (\"decorator.wrapped_f\" . 393))" + ;; Inspired by imenu--flatten-index-alist removed in revno 21853. + (apply + 'nconc + (mapcar + (lambda (item) + (let ((name (if prefix + (concat prefix "." (car item)) + (car item))) + (pos (cdr item))) + (cond ((or (numberp pos) (markerp pos)) + (list (cons name pos))) + ((listp pos) + (cons + (cons name (cdar pos)) + (python-imenu-create-flat-index (cddr item) name)))))) + (or alist + (let* ((fn (lambda (type name) name)) + (python-imenu-format-item-label-function fn) + (python-imenu-format-parent-item-label-function fn) + (python-imenu-format-parent-item-jump-label-function fn)) + (python-imenu-create-index)))))) ;;; Misc helpers @@ -3257,6 +3517,22 @@ Optional argument DIRECTION defines the direction to move to." (goto-char comment-start)) (forward-comment factor))) +(defun python-util-popn (lst n) + "Return LST first N elements. +N should be an integer, when it's a natural negative number its +opposite is used. When N is bigger than the length of LST, the +list is returned as is." + (let* ((n (min (abs n))) + (len (length lst)) + (acc)) + (if (> n len) + lst + (while (< 0 n) + (setq acc (cons (car lst) acc) + lst (cdr lst) + n (1- n))) + (reverse acc)))) + ;;;###autoload (define-derived-mode python-mode prog-mode "Python" @@ -3302,11 +3578,8 @@ if that value is non-nil." (add-hook 'post-self-insert-hook 'python-indent-post-self-insert-function nil 'local) - (set (make-local-variable 'imenu-extract-index-name-function) - #'python-info-current-defun) - - (set (make-local-variable 'imenu-prev-index-position-function) - #'python-imenu-prev-index-position) + (set (make-local-variable 'imenu-create-index-function) + #'python-imenu-create-index) (set (make-local-variable 'add-log-current-defun-function) #'python-info-current-defun) |