X-Git-Url: https://code.delx.au/gnu-emacs/blobdiff_plain/462973b5ef2bdb31392ec8a87ea23d5501059db2..872faefb07a9196a583fc8cbe146ab6a2ebc9c2b:/lisp/progmodes/python.el diff --git a/lisp/progmodes/python.el b/lisp/progmodes/python.el index 1279c38b68..da56fe7032 100644 --- a/lisp/progmodes/python.el +++ b/lisp/progmodes/python.el @@ -1,28 +1,28 @@ ;;; python.el --- Python's flying circus support for Emacs -;; Copyright (C) 2010, 2011 Free Software Foundation, Inc. +;; Copyright (C) 2003-2013 Free Software Foundation, Inc. ;; Author: Fabián E. Gallina ;; URL: https://github.com/fgallina/python.el -;; Version: 0.23.1 +;; Version: 0.24.2 ;; Maintainer: FSF ;; Created: Jul 2010 ;; Keywords: languages -;; This file is NOT part of GNU Emacs. +;; This file is part of GNU Emacs. -;; python.el 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 of the License, or -;; (at your option) any later version. +;; GNU Emacs 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 of the License, +;; or (at your option) any later version. -;; python.el 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. +;; GNU Emacs 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. ;; You should have received a copy of the GNU General Public License -;; along with python.el. If not, see . +;; along with GNU Emacs. If not, see . ;;; Commentary: @@ -30,14 +30,10 @@ ;; indentation bits extracted from original Dave Love's python.el ;; found in GNU/Emacs. -;; While it probably has less features than Dave Love's python.el and -;; PSF's python-mode.el it provides the main stuff you'll need while -;; keeping it simple :) - ;; Implements Syntax highlighting, Indentation, Movement, Shell ;; interaction, Shell completion, Shell virtualenv support, Pdb ;; tracking, Symbol completion, Skeletons, FFAP, Code Check, Eldoc, -;; imenu. +;; Imenu. ;; Syntax highlighting: Fontification of code is provided and supports ;; python's triple quoted strings properly. @@ -50,13 +46,21 @@ ;; Movement: `beginning-of-defun' and `end-of-defun' functions are ;; properly implemented. There are also specialized -;; `forward-sentence' and `backward-sentence' replacements -;; (`python-nav-forward-sentence', `python-nav-backward-sentence' -;; respectively). Extra functions `python-nav-sentence-start' and -;; `python-nav-sentence-end' are included to move to the beginning and -;; to the end of a setence while taking care of multiline definitions. -;; `python-nav-jump-to-defun' is provided and allows jumping to a -;; function or class definition quickly in the current buffer. +;; `forward-sentence' and `backward-sentence' replacements called +;; `python-nav-forward-block', `python-nav-backward-block' +;; respectively which navigate between beginning of blocks of code. +;; Extra functions `python-nav-forward-statement', +;; `python-nav-backward-statement', +;; `python-nav-beginning-of-statement', `python-nav-end-of-statement', +;; `python-nav-beginning-of-block' and `python-nav-end-of-block' are +;; included but no bound to any key. At last but not least the +;; specialized `python-nav-forward-sexp' allows easy navigation +;; between code blocks. If you prefer `cc-mode'-like `forward-sexp' +;; movement, setting `forward-sexp-function' to nil is enough, You can +;; do that using the `python-mode-hook': + +;; (add-hook 'python-mode-hook +;; (lambda () (setq forward-sexp-function nil))) ;; Shell interaction: is provided and allows you to execute easily any ;; block of code of your current buffer in an inferior Python process. @@ -130,7 +134,7 @@ ;; "VIRTUAL_ENV=/path/to/env/")) ;; (python-shell-exec-path . ("/path/to/env/bin/")) -;; Since the above is cumbersome and can be programatically +;; Since the above is cumbersome and can be programmatically ;; calculated, the variable `python-shell-virtualenv-path' is ;; provided. When this variable is set with the path of the ;; virtualenv to use, `process-environment' and `exec-path' get proper @@ -156,7 +160,10 @@ ;; dabbrev. If you have `dabbrev-mode' activated and ;; `python-skeleton-autoinsert' is set to t, then whenever you type ;; the name of any of those defined and hit SPC, they will be -;; automatically expanded. +;; automatically expanded. As an alternative you can use the defined +;; skeleton commands: `python-skeleton-class', `python-skeleton-def' +;; `python-skeleton-for', `python-skeleton-if', `python-skeleton-try' +;; and `python-skeleton-while'. ;; FFAP: You can find the filename for a given module when using ffap ;; out of the box. This feature needs an inferior python shell @@ -170,10 +177,12 @@ ;; might guessed you should run `python-shell-send-buffer' from time ;; to time to get better results too. -;; imenu: This mode supports imenu. It builds a plain or tree menu -;; depending on the value of `python-imenu-make-tree'. Also you can -;; customize if menu items should include its type using -;; `python-imenu-include-defun-type'. +;; 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'. ;; If you used python-mode.el you probably will miss auto-indentation ;; when inserting newlines. To achieve the same behavior you have @@ -204,12 +213,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") @@ -221,7 +228,7 @@ (defgroup python nil "Python Language's flying circus support for Emacs." :group 'languages - :version "23.2" + :version "24.3" :link '(emacs-commentary-link "python")) @@ -230,13 +237,10 @@ (defvar python-mode-map (let ((map (make-sparse-keymap))) ;; Movement - (substitute-key-definition 'backward-sentence - 'python-nav-backward-sentence - map global-map) - (substitute-key-definition 'forward-sentence - 'python-nav-forward-sentence - map global-map) - (define-key map "\C-c\C-j" 'python-nav-jump-to-defun) + (define-key map [remap backward-sentence] 'python-nav-backward-block) + (define-key map [remap forward-sentence] 'python-nav-forward-block) + (define-key map [remap backward-up-list] 'python-nav-backward-up-list) + (define-key map "\C-c\C-j" 'imenu) ;; Indent specific (define-key map "\177" 'python-indent-dedent-line-backspace) (define-key map (kbd "") 'python-indent-dedent-line) @@ -251,6 +255,7 @@ (define-key map "\C-c\C-tt" 'python-skeleton-try) (define-key map "\C-c\C-tw" 'python-skeleton-while) ;; Shell interaction + (define-key map "\C-c\C-p" 'run-python) (define-key map "\C-c\C-s" 'python-shell-send-string) (define-key map "\C-c\C-r" 'python-shell-send-region) (define-key map "\C-\M-x" 'python-shell-send-defun) @@ -277,7 +282,7 @@ :help "Go to end of definition around point"] ["Mark def/class" mark-defun :help "Mark outermost definition around point"] - ["Jump to def/class" python-nav-jump-to-defun + ["Jump to def/class" imenu :help "Jump to a class or function definition"] "--" ("Skeletons") @@ -312,45 +317,102 @@ (eval-when-compile (defconst python-rx-constituents - (list - `(block-start . ,(rx symbol-start + `((block-start . ,(rx symbol-start (or "def" "class" "if" "elif" "else" "try" "except" "finally" "for" "while" "with") symbol-end)) - `(decorator . ,(rx line-start (* space) ?@ (any letter ?_) + (decorator . ,(rx line-start (* space) ?@ (any letter ?_) (* (any word ?_)))) - `(defun . ,(rx symbol-start (or "def" "class") symbol-end)) - `(if-name-main . ,(rx line-start "if" (+ space) "__name__" + (defun . ,(rx symbol-start (or "def" "class") symbol-end)) + (if-name-main . ,(rx line-start "if" (+ space) "__name__" (+ space) "==" (+ space) (any ?' ?\") "__main__" (any ?' ?\") (* space) ?:)) - `(symbol-name . ,(rx (any letter ?_) (* (any word ?_)))) - `(open-paren . ,(rx (or "{" "[" "("))) - `(close-paren . ,(rx (or "}" "]" ")"))) - `(simple-operator . ,(rx (any ?+ ?- ?/ ?& ?^ ?~ ?| ?* ?< ?> ?= ?%))) - `(not-simple-operator . ,(rx + (symbol-name . ,(rx (any letter ?_) (* (any word ?_)))) + (open-paren . ,(rx (or "{" "[" "("))) + (close-paren . ,(rx (or "}" "]" ")"))) + (simple-operator . ,(rx (any ?+ ?- ?/ ?& ?^ ?~ ?| ?* ?< ?> ?= ?%))) + ;; FIXME: rx should support (not simple-operator). + (not-simple-operator . ,(rx (not (any ?+ ?- ?/ ?& ?^ ?~ ?| ?* ?< ?> ?= ?%)))) - `(operator . ,(rx (or "+" "-" "/" "&" "^" "~" "|" "*" "<" ">" + ;; FIXME: Use regexp-opt. + (operator . ,(rx (or "+" "-" "/" "&" "^" "~" "|" "*" "<" ">" "=" "%" "**" "//" "<<" ">>" "<=" "!=" "==" ">=" "is" "not"))) - `(assignment-operator . ,(rx (or "=" "+=" "-=" "*=" "/=" "//=" "%=" "**=" - ">>=" "<<=" "&=" "^=" "|=")))) - "Additional Python specific sexps for `python-rx'")) - -(defmacro python-rx (&rest regexps) - "Python mode specialized rx macro. + ;; FIXME: Use regexp-opt. + (assignment-operator . ,(rx (or "=" "+=" "-=" "*=" "/=" "//=" "%=" "**=" + ">>=" "<<=" "&=" "^=" "|="))) + (string-delimiter . ,(rx (and + ;; Match even number of backslashes. + (or (not (any ?\\ ?\' ?\")) point + ;; Quotes might be preceded by a escaped quote. + (and (or (not (any ?\\)) point) ?\\ + (* ?\\ ?\\) (any ?\' ?\"))) + (* ?\\ ?\\) + ;; Match single or triple quotes of any kind. + (group (or "\"" "\"\"\"" "'" "'''")))))) + "Additional Python specific sexps for `python-rx'") + + (defmacro python-rx (&rest regexps) + "Python mode specialized rx macro. This variant of `rx' supports common python named REGEXPS." - (let ((rx-constituents (append python-rx-constituents rx-constituents))) - (cond ((null regexps) - (error "No regexp")) - ((cdr regexps) - (rx-to-string `(and ,@regexps) t)) - (t - (rx-to-string (car regexps) t))))) + (let ((rx-constituents (append python-rx-constituents rx-constituents))) + (cond ((null regexps) + (error "No regexp")) + ((cdr regexps) + (rx-to-string `(and ,@regexps) t)) + (t + (rx-to-string (car regexps) t)))))) ;;; Font-lock and syntax + +(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 + (lambda (form) + (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))))) + (let ((ppss (or syntax-ppss (syntax-ppss)))) + (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. +The type returned can be `comment', `string' or `paren'." + (let ((ppss (or syntax-ppss (syntax-ppss)))) + (cond + ((nth 8 ppss) (if (nth 4 ppss) 'comment 'string)) + ((nth 1 ppss) 'paren)))) + +(defsubst python-syntax-comment-or-string-p () + "Return non-nil if point is inside 'comment or 'string." + (nth 8 (syntax-ppss))) + +(define-obsolete-function-alias + 'python-info-ppss-context #'python-syntax-context "24.3") + +(define-obsolete-function-alias + 'python-info-ppss-context-type #'python-syntax-context-type "24.3") + +(define-obsolete-function-alias + 'python-info-ppss-comment-or-string-p + #'python-syntax-comment-or-string-p "24.3") + (defvar python-font-lock-keywords ;; Keywords `(,(rx symbol-start @@ -430,17 +492,17 @@ This variant of `rx' supports common python named REGEXPS." ;; Extra: "__all__" "__doc__" "__name__" "__package__") symbol-end) . font-lock-builtin-face) - ;; asignations + ;; assignments ;; support for a = b = c = 5 (,(lambda (limit) (let ((re (python-rx (group (+ (any word ?. ?_))) (? ?\[ (+ (not (any ?\]))) ?\]) (* space) assignment-operator))) (when (re-search-forward re limit t) - (while (and (python-info-ppss-context 'paren) + (while (and (python-syntax-context 'paren) (re-search-forward re limit t))) - (if (and (not (python-info-ppss-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)) @@ -452,60 +514,63 @@ This variant of `rx' supports common python named REGEXPS." assignment-operator))) (when (and (re-search-forward re limit t) (goto-char (nth 3 (match-data)))) - (while (and (python-info-ppss-context 'paren) + (while (and (python-syntax-context 'paren) (re-search-forward re limit t)) (goto-char (nth 3 (match-data)))) - (if (not (python-info-ppss-context 'paren)) + (if (not (python-syntax-context 'paren)) t (set-match-data nil))))) (1 font-lock-variable-name-face nil nil)))) -(defconst python-font-lock-syntactic-keywords - ;; Make outer chars of matching triple-quote sequences into generic - ;; string delimiters. Fixme: Is there a better way? - ;; First avoid a sequence preceded by an odd number of backslashes. - `((,(concat "\\(?:\\([RUru]\\)[Rr]?\\|^\\|[^\\]\\(?:\\\\.\\)*\\)" ;Prefix. - "\\(?:''''''\\|\"\"\"\"\"\"\\)" ; Empty triple-quote - "\\(?:\\('\\)'\\('\\)\\|\\(?2:\"\\)\"\\(?3:\"\\)\\)") - (3 (python-quote-syntax))))) - -(defun python-quote-syntax () - "Put `syntax-table' property correctly on triple quote. -Used for syntactic keywords. N is the match number (1, 2 or 3)." - ;; Given a triple quote, we have to check the context to know - ;; whether this is an opening or closing triple or whether it's - ;; quoted anyhow, and should be ignored. (For that we need to do - ;; the same job as `syntax-ppss' to be correct and it seems to be OK - ;; to use it here despite initial worries.) We also have to sort - ;; out a possible prefix -- well, we don't _have_ to, but I think it - ;; should be treated as part of the string. - - ;; Test cases: - ;; ur"""ar""" x='"' # """ - ;; x = ''' """ ' a - ;; ''' - ;; x '"""' x """ \"""" x - (save-excursion - (goto-char (match-beginning 0)) - (let ((syntax (save-match-data (syntax-ppss)))) - (cond - ((eq t (nth 3 syntax)) ; after unclosed fence - ;; Consider property for the last char if in a fenced string. - (goto-char (nth 8 syntax)) ; fence position - (skip-chars-forward "uUrR") ; skip any prefix - ;; Is it a matching sequence? - (if (eq (char-after) (char-after (match-beginning 2))) - (put-text-property (match-beginning 3) (match-end 3) - 'syntax-table (string-to-syntax "|")))) - ((match-end 1) - ;; Consider property for initial char, accounting for prefixes. - (put-text-property (match-beginning 1) (match-end 1) - 'syntax-table (string-to-syntax "|"))) - (t - ;; Consider property for initial char, accounting for prefixes. - (put-text-property (match-beginning 2) (match-end 2) - 'syntax-table (string-to-syntax "|")))) - ))) +(defconst python-syntax-propertize-function + (syntax-propertize-rules + ((python-rx string-delimiter) + (0 (ignore (python-syntax-stringify)))))) + +(defsubst python-syntax-count-quotes (quote-char &optional point limit) + "Count number of quotes around point (max is 3). +QUOTE-CHAR is the quote char to count. Optional argument POINT is +the point where scan starts (defaults to current point) and LIMIT +is used to limit the scan." + (let ((i 0)) + (while (and (< i 3) + (or (not limit) (< (+ point i) limit)) + (eq (char-after (+ point i)) quote-char)) + (setq i (1+ i))) + i)) + +(defun python-syntax-stringify () + "Put `syntax-table' property correctly on single/triple quotes." + (let* ((num-quotes (length (match-string-no-properties 1))) + (ppss (prog2 + (backward-char num-quotes) + (syntax-ppss) + (forward-char num-quotes))) + (string-start (and (not (nth 4 ppss)) (nth 8 ppss))) + (quote-starting-pos (- (point) num-quotes)) + (quote-ending-pos (point)) + (num-closing-quotes + (and string-start + (python-syntax-count-quotes + (char-before) string-start quote-starting-pos)))) + (cond ((and string-start (= num-closing-quotes 0)) + ;; This set of quotes doesn't match the string starting + ;; kind. Do nothing. + nil) + ((not string-start) + ;; This set of quotes delimit the start of a string. + (put-text-property quote-starting-pos (1+ quote-starting-pos) + 'syntax-table (string-to-syntax "|"))) + ((= num-quotes num-closing-quotes) + ;; This set of quotes delimit the end of a string. + (put-text-property (1- quote-ending-pos) quote-ending-pos + 'syntax-table (string-to-syntax "|"))) + ((> num-quotes num-closing-quotes) + ;; This may only happen whenever a triple quote is closing + ;; a single quoted string. Add string delimiter syntax to + ;; all three quotes. + (put-text-property quote-starting-pos quote-ending-pos + 'syntax-table (string-to-syntax "|")))))) (defvar python-mode-syntax-table (let ((table (make-syntax-table))) @@ -550,6 +615,18 @@ It makes underscores and dots word constituent chars.") :group 'python :safe 'booleanp) +(defcustom python-indent-trigger-commands + '(indent-for-tab-command yas-expand yas/expand) + "Commands that might trigger a `python-indent-line' call." + :type '(repeat symbol) + :group 'python) + +(define-obsolete-variable-alias + 'python-indent 'python-indent-offset "24.3") + +(define-obsolete-variable-alias + 'python-guess-indent 'python-indent-guess-indent-offset "24.3") + (defvar python-indent-current-level 0 "Current indentation level `python-indent-line-function' is using.") @@ -563,6 +640,7 @@ These make `python-indent-calculate-indentation' subtract the value of (defun python-indent-guess-indent-offset () "Guess and set `python-indent-offset' for the current buffer." + (interactive) (save-excursion (save-restriction (widen) @@ -572,7 +650,7 @@ These make `python-indent-calculate-indentation' subtract the value of (re-search-forward (python-rx line-start block-start) nil t)) (when (and - (not (python-info-ppss-context-type)) + (not (python-syntax-context-type)) (progn (goto-char (line-end-position)) (python-util-forward-comment -1) @@ -593,7 +671,7 @@ These make `python-indent-calculate-indentation' subtract the value of (python-util-forward-comment) (current-indentation)))) (if indentation - (setq python-indent-offset indentation) + (set (make-local-variable 'python-indent-offset) indentation) (message "Can't guess python-indent-offset, using defaults: %s" python-indent-offset))))))) @@ -621,19 +699,18 @@ START is the buffer position where the sexp starts." (goto-char (line-beginning-position)) (bobp)) 'no-indent) - ;; Inside a paren - ((setq start (python-info-ppss-context 'paren ppss)) - 'inside-paren) ;; Inside string - ((setq start (python-info-ppss-context 'string ppss)) + ((setq start (python-syntax-context 'string ppss)) 'inside-string) + ;; Inside a paren + ((setq start (python-syntax-context 'paren ppss)) + 'inside-paren) ;; After backslash - ((setq start (when (not (or (python-info-ppss-context 'string ppss) - (python-info-ppss-context 'comment ppss))) - (let ((line-beg-pos (line-beginning-position))) - (when (python-info-line-ends-backslash-p - (1- line-beg-pos)) - (- line-beg-pos 2))))) + ((setq start (when (not (or (python-syntax-context 'string ppss) + (python-syntax-context 'comment ppss))) + (let ((line-beg-pos (line-number-at-pos))) + (python-info-line-ends-backslash-p + (1- line-beg-pos))))) 'after-backslash) ;; After beginning of block ((setq start (save-excursion @@ -647,9 +724,7 @@ START is the buffer position where the sexp starts." (while (and (re-search-backward (python-rx block-start) nil t) (or - (python-info-ppss-context 'string) - (python-info-ppss-context 'comment) - (python-info-ppss-context 'paren) + (python-syntax-context-type) (python-info-continuation-line-p)))) (when (looking-at (python-rx block-start)) (point-marker))))) @@ -657,8 +732,8 @@ START is the buffer position where the sexp starts." ;; After normal line ((setq start (save-excursion (back-to-indentation) - (python-util-forward-comment -1) - (python-nav-sentence-start) + (skip-chars-backward (rx (or whitespace ?\n))) + (python-nav-beginning-of-statement) (point-marker))) 'after-line) ;; Do not indent @@ -673,17 +748,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) @@ -696,11 +771,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 posibilities - ('after-backslash + ;; After backslash we have several possibilities. + (`after-backslash (cond ;; Check if current line is a dot continuation. For this ;; the current line must start with a dot and previous @@ -713,17 +788,13 @@ START is the buffer position where the sexp starts." (while (prog2 (forward-line -1) (and (not (bobp)) - (python-info-ppss-context 'paren)))) + (python-syntax-context 'paren)))) (goto-char (line-end-position)) (while (and (re-search-backward "\\." (line-beginning-position) t) - (or (python-info-ppss-context 'comment) - (python-info-ppss-context 'string) - (python-info-ppss-context 'paren)))) + (python-syntax-context-type))) (if (and (looking-at "\\.") - (not (or (python-info-ppss-context 'comment) - (python-info-ppss-context 'string) - (python-info-ppss-context 'paren)))) + (not (python-syntax-context-type))) ;; The indentation is the same column of the ;; first matching dot that's not inside a ;; comment, a string or a paren @@ -754,12 +825,12 @@ START is the buffer position where the sexp starts." (current-column)))) (t (forward-line -1) - (goto-char (python-info-beginning-of-backlash)) + (goto-char (python-info-beginning-of-backslash)) (if (save-excursion (and (forward-line -1) (goto-char - (or (python-info-beginning-of-backlash) (point))) + (or (python-info-beginning-of-backslash) (point))) (python-info-line-ends-backslash-p))) ;; The two previous lines ended in a backslash so we must ;; respect previous line indentation. @@ -770,16 +841,16 @@ 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 outtermost open paren use the + ;; If current line closes the outermost open paren use the ;; current indentation of the context-start line. ((save-excursion (skip-syntax-forward "\s" (line-end-position)) (when (and (looking-at (regexp-opt '(")" "]" "}"))) (progn (forward-char 1) - (not (python-info-ppss-context 'paren)))) + (not (python-syntax-context 'paren)))) (goto-char context-start) (current-indentation)))) ;; If open paren is contained on a line by itself add another @@ -847,28 +918,40 @@ Uses the offset calculated in indicated by the variable `python-indent-levels' to set the current indentation. -When the variable `last-command' is equal to -`indent-for-tab-command' or FORCE-TOGGLE is non-nil it cycles -levels indicated in the variable `python-indent-levels' by -setting the current level in the variable -`python-indent-current-level'. - -When the variable `last-command' is not equal to -`indent-for-tab-command' and FORCE-TOGGLE is nil it calculates -possible indentation levels and saves it in the variable -`python-indent-levels'. Afterwards it sets the variable -`python-indent-current-level' correctly so offset is equal -to (`nth' `python-indent-current-level' `python-indent-levels')" - (if (or (and (eq this-command 'indent-for-tab-command) - (eq last-command this-command)) - force-toggle) - (if (not (equal python-indent-levels '(0))) - (python-indent-toggle-levels) - (python-indent-calculate-levels)) - (python-indent-calculate-levels)) - (beginning-of-line) - (delete-horizontal-space) - (indent-to (nth python-indent-current-level python-indent-levels)) +When the variable `last-command' is equal to one of the symbols +inside `python-indent-trigger-commands' or FORCE-TOGGLE is +non-nil it cycles levels indicated in the variable +`python-indent-levels' by setting the current level in the +variable `python-indent-current-level'. + +When the variable `last-command' is not equal to one of the +symbols inside `python-indent-trigger-commands' and FORCE-TOGGLE +is nil it calculates possible indentation levels and saves it in +the variable `python-indent-levels'. Afterwards it sets the +variable `python-indent-current-level' correctly so offset is +equal to (`nth' `python-indent-current-level' +`python-indent-levels')" + (or + (and (or (and (memq this-command python-indent-trigger-commands) + (eq last-command this-command)) + force-toggle) + (not (equal python-indent-levels '(0))) + (or (python-indent-toggle-levels) t)) + (python-indent-calculate-levels)) + (let* ((starting-pos (point-marker)) + (indent-ending-position + (+ (line-beginning-position) (current-indentation))) + (follow-indentation-p + (or (bolp) + (and (<= (line-beginning-position) starting-pos) + (>= indent-ending-position starting-pos)))) + (next-indent (nth python-indent-current-level python-indent-levels))) + (unless (= next-indent (current-indentation)) + (beginning-of-line) + (delete-horizontal-space) + (indent-to next-indent) + (goto-char starting-pos)) + (and follow-indentation-p (back-to-indentation))) (python-info-closing-block-message)) (defun python-indent-line-function () @@ -879,8 +962,7 @@ See `python-indent-line' for details." (defun python-indent-dedent-line () "De-indent current line." (interactive "*") - (when (and (not (or (python-info-ppss-context 'string) - (python-info-ppss-context 'comment))) + (when (and (not (python-syntax-comment-or-string-p)) (<= (point-marker) (save-excursion (back-to-indentation) (point-marker))) @@ -914,7 +996,16 @@ Called from a program, START and END specify the region to indent." (back-to-indentation) (setq word (current-word)) (forward-line 1) - (when word + (when (and word + ;; Don't mess with strings, unless it's the + ;; enclosing set of quotes. + (or (not (python-syntax-context 'string)) + (eq + (syntax-after + (+ (1- (point)) + (current-indentation) + (python-syntax-count-quotes (char-after) (point)))) + (string-to-syntax "|")))) (beginning-of-line) (delete-horizontal-space) (indent-to (python-indent-calculate-indentation))))) @@ -971,8 +1062,7 @@ With numeric ARG, just insert that many colons. With (when (and (not arg) (eolp) (not (equal ?: (char-after (- (point-marker) 2)))) - (not (or (python-info-ppss-context 'string) - (python-info-ppss-context 'comment)))) + (not (python-syntax-comment-or-string-p))) (let ((indentation (current-indentation)) (calculated-indentation (python-indent-calculate-indentation))) (python-info-closing-block-message) @@ -996,7 +1086,7 @@ automatically if needed." (goto-char (line-beginning-position)) ;; If after going to the beginning of line the point ;; is still inside a paren it's ok to do the trick - (when (python-info-ppss-context 'paren) + (when (python-syntax-context 'paren) (let ((indentation (python-indent-calculate-indentation))) (when (< (current-indentation) indentation) (indent-line-to indentation))))))) @@ -1010,188 +1100,414 @@ automatically if needed." The name of the defun should be grouped so it can be retrieved via `match-string'.") -(defun python-nav-beginning-of-defun (&optional nodecorators) +(defun python-nav--beginning-of-defun (&optional arg) + "Internal implementation of `python-nav-beginning-of-defun'. +With positive ARG search backwards, else search forwards." + (when (or (null arg) (= arg 0)) (setq arg 1)) + (let* ((re-search-fn (if (> arg 0) + #'re-search-backward + #'re-search-forward)) + (line-beg-pos (line-beginning-position)) + (line-content-start (+ line-beg-pos (current-indentation))) + (pos (point-marker)) + (beg-indentation + (and (> arg 0) + (save-excursion + (while (and + (not (python-info-looking-at-beginning-of-defun)) + (python-nav-backward-block))) + (or (and (python-info-looking-at-beginning-of-defun) + (+ (current-indentation) python-indent-offset)) + 0)))) + (found + (progn + (when (and (< arg 0) + (python-info-looking-at-beginning-of-defun)) + (end-of-line 1)) + (while (and (funcall re-search-fn + python-nav-beginning-of-defun-regexp nil t) + (or (python-syntax-context-type) + ;; Handle nested defuns when moving + ;; backwards by checking indentation. + (and (> arg 0) + (not (= (current-indentation) 0)) + (>= (current-indentation) beg-indentation))))) + (and (python-info-looking-at-beginning-of-defun) + (or (not (= (line-number-at-pos pos) + (line-number-at-pos))) + (and (>= (point) line-beg-pos) + (<= (point) line-content-start) + (> pos line-content-start))))))) + (if found + (or (beginning-of-line 1) t) + (and (goto-char pos) nil)))) + +(defun python-nav-beginning-of-defun (&optional arg) "Move point to `beginning-of-defun'. -When NODECORATORS is non-nil decorators are not included. This -is the main part of`python-beginning-of-defun-function' -implementation. Return non-nil if point is moved to the +With positive ARG search backwards else search forward. When ARG +is nil or 0 defaults to 1. When searching backwards nested +defuns are handled with care depending on current point +position. Return non-nil if point is moved to `beginning-of-defun'." - (let ((indent-pos (save-excursion - (back-to-indentation) - (point-marker))) - (found) - (include-decorators - (lambda () - (when (not nodecorators) - (when (save-excursion - (forward-line -1) - (looking-at (python-rx decorator))) - (while (and (not (bobp)) - (forward-line -1) - (looking-at (python-rx decorator)))) - (when (not (bobp)) (forward-line 1))))))) - (if (and (> (point) indent-pos) - (save-excursion - (goto-char (line-beginning-position)) - (looking-at python-nav-beginning-of-defun-regexp))) - (progn - (goto-char (line-beginning-position)) - (funcall include-decorators) - (setq found t)) - (goto-char (line-beginning-position)) - (when (re-search-backward python-nav-beginning-of-defun-regexp nil t) - (setq found t)) - (goto-char (or (python-info-ppss-context 'string) (point))) - (funcall include-decorators)) + (when (or (null arg) (= arg 0)) (setq arg 1)) + (let ((found)) + (cond ((and (eq this-command 'mark-defun) + (python-info-looking-at-beginning-of-defun))) + (t + (dotimes (i (if (> arg 0) arg (- arg))) + (when (and (python-nav--beginning-of-defun arg) + (not found)) + (setq found t))))) found)) -(defun python-beginning-of-defun-function (&optional arg nodecorators) - "Move point to the beginning of def or class. -With positive ARG move that number of functions forward. With -negative do the same but backwards. When NODECORATORS is non-nil -decorators are not included. Return non-nil if point is moved to the -`beginning-of-defun'." - (when (or (null arg) (= arg 0)) (setq arg 1)) - (cond ((and (eq this-command 'mark-defun) - (looking-at python-nav-beginning-of-defun-regexp))) - ((> arg 0) - (dotimes (i arg (python-nav-beginning-of-defun nodecorators)))) - (t - (let ((found)) - (dotimes (i (- arg) found) - (python-end-of-defun-function) - (python-util-forward-comment) - (goto-char (line-end-position)) - (when (not (eobp)) - (setq found - (python-nav-beginning-of-defun nodecorators)))))))) - -(defun python-end-of-defun-function () +(defun python-nav-end-of-defun () "Move point to the end of def or class. Returns nil if point is not in a def or class." (interactive) (let ((beg-defun-indent) - (decorator-regexp "[[:space:]]*@")) - (when (looking-at decorator-regexp) - (while (and (not (eobp)) - (forward-line 1) - (looking-at decorator-regexp)))) - (when (not (looking-at python-nav-beginning-of-defun-regexp)) - (python-beginning-of-defun-function)) - (setq beg-defun-indent (current-indentation)) - (forward-line 1) - (while (and (forward-line 1) - (not (eobp)) - (or (not (current-word)) - (equal (char-after (+ (point) (current-indentation))) ?#) - (> (current-indentation) beg-defun-indent) - (not (looking-at python-nav-beginning-of-defun-regexp))))) - (python-util-forward-comment) - (goto-char (line-beginning-position)))) + (beg-pos (point))) + (when (or (python-info-looking-at-beginning-of-defun) + (python-nav-beginning-of-defun 1) + (python-nav-beginning-of-defun -1)) + (setq beg-defun-indent (current-indentation)) + (while (progn + (python-nav-end-of-statement) + (python-util-forward-comment 1) + (and (> (current-indentation) beg-defun-indent) + (not (eobp))))) + (python-util-forward-comment -1) + (forward-line 1) + ;; Ensure point moves forward. + (and (> beg-pos (point)) (goto-char beg-pos))))) -(defun python-nav-sentence-start () - "Move to start of current sentence." +(defun python-nav-beginning-of-statement () + "Move to start of current statement." (interactive "^") - (while (and (not (back-to-indentation)) + (while (and (or (back-to-indentation) t) (not (bobp)) (when (or (save-excursion (forward-line -1) (python-info-line-ends-backslash-p)) - (python-info-ppss-context 'string) - (python-info-ppss-context 'paren)) - (forward-line -1))))) - -(defun python-nav-sentence-end () - "Move to end of current sentence." + (python-syntax-context 'string) + (python-syntax-context 'paren)) + (forward-line -1)))) + (point-marker)) + +(defun python-nav-end-of-statement (&optional noend) + "Move to end of current statement. +Optional argument NOEND is internal and makes the logic to not +jump to the end of line when moving forward searching for the end +of the statement." (interactive "^") - (while (and (goto-char (line-end-position)) - (not (eobp)) - (when (or - (python-info-line-ends-backslash-p) - (python-info-ppss-context 'string) - (python-info-ppss-context 'paren)) - (forward-line 1))))) - -(defun python-nav-backward-sentence (&optional arg) - "Move backward to start of sentence. With ARG, do it arg times. -See `python-nav-forward-sentence' for more information." + (let (string-start bs-pos) + (while (and (or noend (goto-char (line-end-position))) + (not (eobp)) + (cond ((setq string-start (python-syntax-context 'string)) + (goto-char string-start) + (if (python-syntax-context 'paren) + ;; Ended up inside a paren, roll again. + (python-nav-end-of-statement t) + ;; This is not inside a paren, move to the + ;; end of this string. + (goto-char (+ (point) + (python-syntax-count-quotes + (char-after (point)) (point)))) + (or (re-search-forward (rx (syntax string-delimiter)) nil t) + (goto-char (point-max))))) + ((python-syntax-context 'paren) + ;; The statement won't end before we've escaped + ;; at least one level of parenthesis. + (condition-case err + (goto-char (scan-lists (point) 1 -1)) + (scan-error (goto-char (nth 3 err))))) + ((setq bs-pos (python-info-line-ends-backslash-p)) + (goto-char bs-pos) + (forward-line 1)))))) + (point-marker)) + +(defun python-nav-backward-statement (&optional arg) + "Move backward to previous statement. +With ARG, repeat. See `python-nav-forward-statement'." (interactive "^p") (or arg (setq arg 1)) - (python-nav-forward-sentence (- arg))) + (python-nav-forward-statement (- arg))) -(defun python-nav-forward-sentence (&optional arg) - "Move forward to next end of sentence. With ARG, repeat. -With negative argument, move backward repeatedly to start of sentence." +(defun python-nav-forward-statement (&optional arg) + "Move forward to next statement. +With ARG, repeat. With negative argument, move ARG times +backward to previous statement." (interactive "^p") (or arg (setq arg 1)) (while (> arg 0) + (python-nav-end-of-statement) (python-util-forward-comment) - (python-nav-sentence-end) - (forward-line 1) + (python-nav-beginning-of-statement) (setq arg (1- arg))) (while (< arg 0) - (python-nav-sentence-end) + (python-nav-beginning-of-statement) (python-util-forward-comment -1) - (python-nav-sentence-start) - (forward-line -1) + (python-nav-beginning-of-statement) (setq arg (1+ arg)))) -(defvar python-nav-list-defun-positions-cache nil) -(make-variable-buffer-local 'python-nav-list-defun-positions-cache) - -(defun python-nav-list-defun-positions (&optional include-type rescan) - "Make an Alist of defun names and point markers for current buffer. -When optional argument INCLUDE-TYPE is non-nil the type is -included the defun name. With optional argument RESCAN the -`python-nav-list-defun-positions-cache' is invalidated and the -list of defun is regenerated again." - (if (and python-nav-list-defun-positions-cache (not rescan)) - python-nav-list-defun-positions-cache - (let ((defs)) - (save-restriction - (widen) - (save-excursion - (goto-char (point-max)) - (while (re-search-backward python-nav-beginning-of-defun-regexp nil t) - (when (and (not (python-info-ppss-context 'string)) - (not (python-info-ppss-context 'comment)) - (not (python-info-ppss-context 'parent))) - (add-to-list - 'defs (cons - (python-info-current-defun include-type) - (point-marker))))) - (setq python-nav-list-defun-positions-cache defs)))))) - -(defun python-nav-read-defun (&optional rescan) - "Read a defun name of current buffer and return its point marker. -A cons cell with the form (DEFUN-NAME . POINT-MARKER) is returned -when defun is completed, else nil. With optional argument RESCAN -forces `python-nav-list-defun-positions' to invalidate its -cache." - (let ((defs (python-nav-list-defun-positions nil rescan))) - (minibuffer-with-setup-hook - (lambda () - (setq minibuffer-completion-table (mapcar 'car defs))) - (let ((stringdef - (read-from-minibuffer - "Jump to definition: " nil - minibuffer-local-must-match-map))) - (when (not (string= stringdef "")) - (assoc-string stringdef defs)))))) - -(defun python-nav-jump-to-defun (def) - "Jump to the definition of DEF in current file. -Locations are cached; use a C-u prefix argument to force a -rescan." - (interactive - (list (python-nav-read-defun current-prefix-arg))) - (when (not (called-interactively-p 'interactive)) - (setq def (assoc-string def (python-nav-list-defun-positions)))) - (let ((def-marker (cdr def))) - (when (markerp def-marker) - (goto-char (marker-position def-marker)) - (back-to-indentation)))) +(defun python-nav-beginning-of-block () + "Move to start of current block." + (interactive "^") + (let ((starting-pos (point)) + (block-regexp (python-rx + line-start (* whitespace) block-start))) + (if (progn + (python-nav-beginning-of-statement) + (looking-at (python-rx block-start))) + (point-marker) + ;; Go to first line beginning a statement + (while (and (not (bobp)) + (or (and (python-nav-beginning-of-statement) nil) + (python-info-current-line-comment-p) + (python-info-current-line-empty-p))) + (forward-line -1)) + (let ((block-matching-indent + (- (current-indentation) python-indent-offset))) + (while + (and (python-nav-backward-block) + (> (current-indentation) block-matching-indent))) + (if (and (looking-at (python-rx block-start)) + (= (current-indentation) block-matching-indent)) + (point-marker) + (and (goto-char starting-pos) nil)))))) + +(defun python-nav-end-of-block () + "Move to end of current block." + (interactive "^") + (when (python-nav-beginning-of-block) + (let ((block-indentation (current-indentation))) + (python-nav-end-of-statement) + (while (and (forward-line 1) + (not (eobp)) + (or (and (> (current-indentation) block-indentation) + (or (python-nav-end-of-statement) t)) + (python-info-current-line-comment-p) + (python-info-current-line-empty-p)))) + (python-util-forward-comment -1) + (point-marker)))) + +(defun python-nav-backward-block (&optional arg) + "Move backward to previous block of code. +With ARG, repeat. See `python-nav-forward-block'." + (interactive "^p") + (or arg (setq arg 1)) + (python-nav-forward-block (- arg))) + +(defun python-nav-forward-block (&optional arg) + "Move forward to next block of code. +With ARG, repeat. With negative argument, move ARG times +backward to previous block." + (interactive "^p") + (or arg (setq arg 1)) + (let ((block-start-regexp + (python-rx line-start (* whitespace) block-start)) + (starting-pos (point))) + (while (> arg 0) + (python-nav-end-of-statement) + (while (and + (re-search-forward block-start-regexp nil t) + (python-syntax-context-type))) + (setq arg (1- arg))) + (while (< arg 0) + (python-nav-beginning-of-statement) + (while (and + (re-search-backward block-start-regexp nil t) + (python-syntax-context-type))) + (setq arg (1+ arg))) + (python-nav-beginning-of-statement) + (if (not (looking-at (python-rx block-start))) + (and (goto-char starting-pos) nil) + (and (not (= (point) starting-pos)) (point-marker))))) + +(defun python-nav-lisp-forward-sexp-safe (&optional arg) + "Safe version of standard `forward-sexp'. +When ARG > 0 move forward, else if ARG is < 0." + (or arg (setq arg 1)) + (let ((forward-sexp-function) + (paren-regexp + (if (> arg 0) (python-rx close-paren) (python-rx open-paren))) + (search-fn + (if (> arg 0) #'re-search-forward #'re-search-backward))) + (condition-case nil + (forward-sexp arg) + (error + (while (and (funcall search-fn paren-regexp nil t) + (python-syntax-context 'paren))))))) + +(defun python-nav--forward-sexp (&optional dir) + "Move to forward sexp. +With positive Optional argument DIR direction move forward, else +backwards." + (setq dir (or dir 1)) + (unless (= dir 0) + (let* ((forward-p (if (> dir 0) + (and (setq dir 1) t) + (and (setq dir -1) nil))) + (re-search-fn (if forward-p + 're-search-forward + 're-search-backward)) + (context-type (python-syntax-context-type))) + (cond + ((memq context-type '(string comment)) + ;; Inside of a string, get out of it. + (let ((forward-sexp-function)) + (forward-sexp dir))) + ((or (eq context-type 'paren) + (and forward-p (looking-at (python-rx open-paren))) + (and (not forward-p) + (eq (syntax-class (syntax-after (1- (point)))) + (car (string-to-syntax ")"))))) + ;; Inside a paren or looking at it, lisp knows what to do. + (python-nav-lisp-forward-sexp-safe dir)) + (t + ;; This part handles the lispy feel of + ;; `python-nav-forward-sexp'. Knowing everything about the + ;; current context and the context of the next sexp tries to + ;; follow the lisp sexp motion commands in a symmetric manner. + (let* ((context + (cond + ((python-info-beginning-of-block-p) 'block-start) + ((python-info-end-of-block-p) 'block-end) + ((python-info-beginning-of-statement-p) 'statement-start) + ((python-info-end-of-statement-p) 'statement-end))) + (next-sexp-pos + (save-excursion + (python-nav-lisp-forward-sexp-safe dir) + (point))) + (next-sexp-context + (save-excursion + (goto-char next-sexp-pos) + (cond + ((python-info-beginning-of-block-p) 'block-start) + ((python-info-end-of-block-p) 'block-end) + ((python-info-beginning-of-statement-p) 'statement-start) + ((python-info-end-of-statement-p) 'statement-end) + ((python-info-statement-starts-block-p) 'starts-block) + ((python-info-statement-ends-block-p) 'ends-block))))) + (if forward-p + (cond ((and (not (eobp)) + (python-info-current-line-empty-p)) + (python-util-forward-comment dir) + (python-nav--forward-sexp dir)) + ((eq context 'block-start) + (python-nav-end-of-block)) + ((eq context 'statement-start) + (python-nav-end-of-statement)) + ((and (memq context '(statement-end block-end)) + (eq next-sexp-context 'ends-block)) + (goto-char next-sexp-pos) + (python-nav-end-of-block)) + ((and (memq context '(statement-end block-end)) + (eq next-sexp-context 'starts-block)) + (goto-char next-sexp-pos) + (python-nav-end-of-block)) + ((memq context '(statement-end block-end)) + (goto-char next-sexp-pos) + (python-nav-end-of-statement)) + (t (goto-char next-sexp-pos))) + (cond ((and (not (bobp)) + (python-info-current-line-empty-p)) + (python-util-forward-comment dir) + (python-nav--forward-sexp dir)) + ((eq context 'block-end) + (python-nav-beginning-of-block)) + ((eq context 'statement-end) + (python-nav-beginning-of-statement)) + ((and (memq context '(statement-start block-start)) + (eq next-sexp-context 'starts-block)) + (goto-char next-sexp-pos) + (python-nav-beginning-of-block)) + ((and (memq context '(statement-start block-start)) + (eq next-sexp-context 'ends-block)) + (goto-char next-sexp-pos) + (python-nav-beginning-of-block)) + ((memq context '(statement-start block-start)) + (goto-char next-sexp-pos) + (python-nav-beginning-of-statement)) + (t (goto-char next-sexp-pos)))))))))) + +(defun python-nav--backward-sexp () + "Move to backward sexp." + (python-nav--forward-sexp -1)) + +(defun python-nav-forward-sexp (&optional arg) + "Move forward across one block of code. +With ARG, do it that many times. Negative arg -N means +move backward N times." + (interactive "^p") + (or arg (setq arg 1)) + (while (> arg 0) + (python-nav--forward-sexp) + (setq arg (1- arg))) + (while (< arg 0) + (python-nav--backward-sexp) + (setq arg (1+ arg)))) + +(defun python-nav--up-list (&optional dir) + "Internal implementation of `python-nav-up-list'. +DIR is always 1 or -1 and comes sanitized from +`python-nav-up-list' calls." + (let ((context (python-syntax-context-type)) + (forward-p (> dir 0))) + (cond + ((memq context '(string comment))) + ((eq context 'paren) + (let ((forward-sexp-function)) + (up-list dir))) + ((and forward-p (python-info-end-of-block-p)) + (let ((parent-end-pos + (save-excursion + (let ((indentation (and + (python-nav-beginning-of-block) + (current-indentation)))) + (while (and indentation + (> indentation 0) + (>= (current-indentation) indentation) + (python-nav-backward-block))) + (python-nav-end-of-block))))) + (and (> (or parent-end-pos (point)) (point)) + (goto-char parent-end-pos)))) + (forward-p (python-nav-end-of-block)) + ((and (not forward-p) + (> (current-indentation) 0) + (python-info-beginning-of-block-p)) + (let ((prev-block-pos + (save-excursion + (let ((indentation (current-indentation))) + (while (and (python-nav-backward-block) + (>= (current-indentation) indentation)))) + (point)))) + (and (> (point) prev-block-pos) + (goto-char prev-block-pos)))) + ((not forward-p) (python-nav-beginning-of-block))))) + +(defun python-nav-up-list (&optional arg) + "Move forward out of one level of parentheses (or blocks). +With ARG, do this that many times. +A negative argument means move backward but still to a less deep spot. +This command assumes point is not in a string or comment." + (interactive "^p") + (or arg (setq arg 1)) + (while (> arg 0) + (python-nav--up-list 1) + (setq arg (1- arg))) + (while (< arg 0) + (python-nav--up-list -1) + (setq arg (1+ arg)))) + +(defun python-nav-backward-up-list (&optional arg) + "Move backward out of one level of parentheses (or blocks). +With ARG, do this that many times. +A negative argument means move backward but still to a less deep spot. +This command assumes point is not in a string or comment." + (interactive "^p") + (or arg (setq arg 1)) + (python-nav-up-list (- arg))) ;;; Shell integration @@ -1253,14 +1569,6 @@ Restart the python shell after changing this variable for it to take effect." :group 'python :safe 'booleanp) -(defcustom python-shell-send-setup-max-wait 5 - "Seconds to wait for process output before code setup. -If output is received before the especified time then control is -returned in that moment and not after waiting." - :type 'integer - :group 'python - :safe 'integerp) - (defcustom python-shell-process-environment nil "List of environment variables for Python shell. This variable follows the same rules as `process-environment' @@ -1327,16 +1635,12 @@ virtualenv." If DEDICATED is t and the variable `buffer-file-name' is non-nil returns a string with the form `python-shell-buffer-name'[variable `buffer-file-name'] else -returns the value of `python-shell-buffer-name'. After -calculating the process name adds the buffer name for the process -in the `same-window-buffer-names' list." +returns the value of `python-shell-buffer-name'." (let ((process-name (if (and dedicated buffer-file-name) (format "%s[%s]" python-shell-buffer-name buffer-file-name) (format "%s" python-shell-buffer-name)))) - (add-to-list 'same-window-buffer-names (purecopy - (format "*%s*" process-name))) process-name)) (defun python-shell-internal-get-process-name () @@ -1361,7 +1665,11 @@ uniqueness for different types of configurations." (defun python-shell-parse-command () "Calculate the string used to execute the inferior Python process." - (format "%s %s" python-shell-interpreter python-shell-interpreter-args)) + (let ((process-environment (python-shell-calculate-process-environment)) + (exec-path (python-shell-calculate-exec-path))) + (format "%s %s" + (executable-find python-shell-interpreter) + python-shell-interpreter-args))) (defun python-shell-calculate-process-environment () "Calculate process environment given `python-shell-virtualenv-path'." @@ -1403,6 +1711,22 @@ uniqueness for different types of configurations." OUTPUT is a string with the contents of the buffer." (ansi-color-filter-apply output)) +(defvar python-shell--parent-buffer nil) + +(defvar python-shell-output-syntax-table + (let ((table (make-syntax-table python-dotty-syntax-table))) + (modify-syntax-entry ?\' "." table) + (modify-syntax-entry ?\" "." table) + (modify-syntax-entry ?\( "." table) + (modify-syntax-entry ?\[ "." table) + (modify-syntax-entry ?\{ "." table) + (modify-syntax-entry ?\) "." table) + (modify-syntax-entry ?\] "." table) + (modify-syntax-entry ?\} "." table) + table) + "Syntax table for shell output. +It makes parens and quotes be treated as punctuation chars.") + (define-derived-mode inferior-python-mode comint-mode "Inferior Python" "Major mode for Python inferior process. Runs a Python interpreter as a subprocess of Emacs, with Python @@ -1425,12 +1749,13 @@ initialization of the interpreter via `python-shell-setup-codes' variable. \(Type \\[describe-mode] in the process buffer for a list of commands.)" - (set-syntax-table python-mode-syntax-table) - (setq mode-line-process '(":%s")) + (and python-shell--parent-buffer + (python-util-clone-local-variables python-shell--parent-buffer)) (setq comint-prompt-regexp (format "^\\(?:%s\\|%s\\|%s\\)" python-shell-prompt-regexp python-shell-prompt-block-regexp python-shell-prompt-pdb-regexp)) + (setq mode-line-process '(":%s")) (make-local-variable 'comint-output-filter-functions) (add-hook 'comint-output-filter-functions 'python-comint-output-filter-function) @@ -1444,80 +1769,112 @@ variable. 'python-shell-completion-complete-at-point nil 'local) (add-to-list (make-local-variable 'comint-dynamic-complete-functions) 'python-shell-completion-complete-at-point) - (define-key inferior-python-mode-map (kbd "") + (define-key inferior-python-mode-map "\t" 'python-shell-completion-complete-or-indent) + (make-local-variable 'python-pdbtrack-buffers-to-kill) + (make-local-variable 'python-pdbtrack-tracked-buffer) + (make-local-variable 'python-shell-internal-last-output) (when python-shell-enable-font-lock - (set - (make-local-variable 'font-lock-defaults) - '(python-font-lock-keywords - nil nil nil nil - (font-lock-syntactic-keywords . python-font-lock-syntactic-keywords)))) + (set-syntax-table python-mode-syntax-table) + (set (make-local-variable 'font-lock-defaults) + '(python-font-lock-keywords nil nil nil nil)) + (set (make-local-variable 'syntax-propertize-function) + (eval + ;; XXX: Unfortunately eval is needed here to make use of the + ;; dynamic value of `comint-prompt-regexp'. + `(syntax-propertize-rules + (,comint-prompt-regexp + (0 (ignore + (put-text-property + comint-last-input-start end 'syntax-table + python-shell-output-syntax-table) + ;; XXX: This might look weird, but it is the easiest + ;; way to ensure font lock gets cleaned up before the + ;; current prompt, which is needed for unclosed + ;; strings to not mess up with current input. + (font-lock-unfontify-region comint-last-input-start end)))) + (,(python-rx string-delimiter) + (0 (ignore + (and (not (eq (get-text-property start 'field) 'output)) + (python-syntax-stringify))))))))) (compilation-shell-minor-mode 1)) -(defun python-shell-make-comint (cmd proc-name &optional pop) +(defun python-shell-make-comint (cmd proc-name &optional pop internal) "Create a python shell comint buffer. CMD is the python command to be executed and PROC-NAME is the process name the comint buffer will get. After the comint buffer -is created the `inferior-python-mode' is activated. If POP is -non-nil the buffer is shown." +is created the `inferior-python-mode' is activated. When +optional argument POP is non-nil the buffer is shown. When +optional argument INTERNAL is non-nil this process is run on a +buffer with a name that starts with a space, following the Emacs +convention for temporary/internal buffers, and also makes sure +the user is not queried for confirmation when the process is +killed." (save-excursion - (let* ((proc-buffer-name (format "*%s*" proc-name)) + (let* ((proc-buffer-name + (format (if (not internal) "*%s*" " *%s*") proc-name)) (process-environment (python-shell-calculate-process-environment)) (exec-path (python-shell-calculate-exec-path))) (when (not (comint-check-proc proc-buffer-name)) (let* ((cmdlist (split-string-and-unquote cmd)) - (buffer (apply 'make-comint proc-name (car cmdlist) nil - (cdr cmdlist))) - (current-buffer (current-buffer))) + (buffer (apply #'make-comint-in-buffer proc-name proc-buffer-name + (car cmdlist) nil (cdr cmdlist))) + (python-shell--parent-buffer (current-buffer)) + (process (get-buffer-process buffer))) (with-current-buffer buffer - (inferior-python-mode) - (python-util-clone-local-variables current-buffer)))) - (when pop - (pop-to-buffer proc-buffer-name)) + (inferior-python-mode)) + (accept-process-output process) + (and pop (pop-to-buffer buffer t)) + (and internal (set-process-query-on-exit-flag process nil)))) proc-buffer-name))) -(defun run-python (dedicated cmd) +;;;###autoload +(defun run-python (cmd &optional dedicated show) "Run an inferior Python process. Input and output via buffer named after `python-shell-buffer-name'. If there is a process already running in that buffer, just switch to it. -With argument, allows you to define DEDICATED, so a dedicated -process for the current buffer is open, and define CMD so you can -edit the command used to call the interpreter (default is value -of `python-shell-interpreter' and arguments defined in -`python-shell-interpreter-args'). Runs the hook -`inferior-python-mode-hook' (after the `comint-mode-hook' is -run). -\(Type \\[describe-mode] in the process buffer for a list of commands.)" + +With argument, allows you to define CMD so you can edit the +command used to call the interpreter and define DEDICATED, so a +dedicated process for the current buffer is open. When numeric +prefix arg is other than 0 or 4 do not SHOW. + +Runs the hook `inferior-python-mode-hook' (after the +`comint-mode-hook' is run). \(Type \\[describe-mode] in the +process buffer for a list of commands.)" (interactive (if current-prefix-arg (list + (read-string "Run Python: " (python-shell-parse-command)) (y-or-n-p "Make dedicated process? ") - (read-string "Run Python: " (python-shell-parse-command))) - (list nil (python-shell-parse-command)))) - (python-shell-make-comint cmd (python-shell-get-process-name dedicated)) + (= (prefix-numeric-value current-prefix-arg) 4)) + (list (python-shell-parse-command) nil t))) + (python-shell-make-comint + cmd (python-shell-get-process-name dedicated) show) dedicated) (defun run-python-internal () "Run an inferior Internal Python process. Input and output via buffer named after `python-shell-internal-buffer-name' and what -`python-shell-internal-get-process-name' returns. This new kind -of shell is intended to be used for generic communication related -to defined configurations. The main difference with global or -dedicated shells is that these ones are attached to a -configuration, not a buffer. This means that can be used for -example to retrieve the sys.path and other stuff, without messing -with user shells. Runs the hook -`inferior-python-mode-hook' (after the `comint-mode-hook' is -run). \(Type \\[describe-mode] in the process buffer for a list -of commands.)" - (interactive) - (set-process-query-on-exit-flag - (get-buffer-process - (python-shell-make-comint - (python-shell-parse-command) - (python-shell-internal-get-process-name))) nil)) +`python-shell-internal-get-process-name' returns. + +This new kind of shell is intended to be used for generic +communication related to defined configurations, the main +difference with global or dedicated shells is that these ones are +attached to a configuration, not a buffer. This means that can +be used for example to retrieve the sys.path and other stuff, +without messing with user shells. Note that +`python-shell-enable-font-lock' and `inferior-python-mode-hook' +are set to nil for these shells, so setup codes are not sent at +startup." + (let ((python-shell-enable-font-lock nil) + (inferior-python-mode-hook nil)) + (get-buffer-process + (python-shell-make-comint + (python-shell-parse-command) + (python-shell-internal-get-process-name) nil t)))) (defun python-shell-get-process () "Get inferior Python process for current buffer and return it." @@ -1539,7 +1896,7 @@ of commands.)" (global-proc-buffer-name (format "*%s*" global-proc-name)) (dedicated-running (comint-check-proc dedicated-proc-buffer-name)) (global-running (comint-check-proc global-proc-buffer-name)) - (current-prefix-arg 4)) + (current-prefix-arg 16)) (when (and (not dedicated-running) (not global-running)) (if (call-interactively 'run-python) (setq dedicated-running t) @@ -1553,21 +1910,35 @@ of commands.)" "Current internal shell buffer for the current buffer. This is really not necessary at all for the code to work but it's there for compatibility with CEDET.") -(make-variable-buffer-local 'python-shell-internal-buffer) + +(defvar python-shell-internal-last-output nil + "Last output captured by the internal shell. +This is really not necessary at all for the code to work but it's +there for compatibility with CEDET.") (defun python-shell-internal-get-or-create-process () "Get or create an inferior Internal Python process." (let* ((proc-name (python-shell-internal-get-process-name)) - (proc-buffer-name (format "*%s*" proc-name))) - (run-python-internal) - (setq python-shell-internal-buffer proc-buffer-name) + (proc-buffer-name (format " *%s*" proc-name))) + (when (not (process-live-p proc-name)) + (run-python-internal) + (setq python-shell-internal-buffer proc-buffer-name) + ;; XXX: Why is this `sit-for' needed? + ;; `python-shell-make-comint' calls `accept-process-output' + ;; already but it is not helping to get proper output on + ;; 'gnu/linux when the internal shell process is not running and + ;; a call to `python-shell-internal-send-string' is issued. + (sit-for 0.1 t)) (get-buffer-process proc-buffer-name))) (define-obsolete-function-alias - 'python-proc 'python-shell-internal-get-or-create-process "23.3") + 'python-proc 'python-shell-internal-get-or-create-process "24.3") + +(define-obsolete-variable-alias + 'python-buffer 'python-shell-internal-buffer "24.3") (define-obsolete-variable-alias - 'python-buffer 'python-shell-internal-buffer "23.3") + 'python-preoutput-result 'python-shell-internal-last-output "24.3") (defun python-shell-send-string (string &optional process msg) "Send STRING to inferior Python PROCESS. @@ -1575,10 +1946,13 @@ When MSG is non-nil messages the first line of STRING." (interactive "sPython command: ") (let ((process (or process (python-shell-get-or-create-process))) (lines (split-string string "\n" t))) - (when msg - (message (format "Sent: %s..." (nth 0 lines)))) + (and msg (message "Sent: %s..." (nth 0 lines))) (if (> (length lines) 1) - (let* ((temp-file-name (make-temp-file "py")) + (let* ((temporary-file-directory + (if (file-remote-p default-directory) + (concat (file-remote-p default-directory) "/tmp") + temporary-file-directory)) + (temp-file-name (make-temp-file "py")) (file-name (or (buffer-file-name) temp-file-name))) (with-temp-file temp-file-name (insert string) @@ -1589,78 +1963,129 @@ When MSG is non-nil messages the first line of STRING." (string-match "\n[ \t].*\n?$" string)) (comint-send-string process "\n"))))) +(defvar python-shell-output-filter-in-progress nil) +(defvar python-shell-output-filter-buffer nil) + +(defun python-shell-output-filter (string) + "Filter used in `python-shell-send-string-no-output' to grab output. +STRING is the output received to this point from the process. +This filter saves received output from the process in +`python-shell-output-filter-buffer' and stops receiving it after +detecting a prompt at the end of the buffer." + (setq + string (ansi-color-filter-apply string) + python-shell-output-filter-buffer + (concat python-shell-output-filter-buffer string)) + (when (string-match + ;; XXX: It seems on OSX an extra carriage return is attached + ;; at the end of output, this handles that too. + (format "\r?\n\\(?:%s\\|%s\\|%s\\)$" + python-shell-prompt-regexp + python-shell-prompt-block-regexp + python-shell-prompt-pdb-regexp) + python-shell-output-filter-buffer) + ;; Output ends when `python-shell-output-filter-buffer' contains + ;; the prompt attached at the end of it. + (setq python-shell-output-filter-in-progress nil + python-shell-output-filter-buffer + (substring python-shell-output-filter-buffer + 0 (match-beginning 0))) + (when (and (> (length python-shell-prompt-output-regexp) 0) + (string-match (concat "^" python-shell-prompt-output-regexp) + python-shell-output-filter-buffer)) + ;; Some shells, like iPython might append a prompt before the + ;; output, clean that. + (setq python-shell-output-filter-buffer + (substring python-shell-output-filter-buffer (match-end 0))))) + "") + (defun python-shell-send-string-no-output (string &optional process msg) "Send STRING to PROCESS and inhibit output. When MSG is non-nil messages the first line of STRING. Return the output." - (let* ((output-buffer) - (process (or process (python-shell-get-or-create-process))) - (comint-preoutput-filter-functions - (append comint-preoutput-filter-functions - '(ansi-color-filter-apply - (lambda (string) - (setq output-buffer (concat output-buffer string)) - ""))))) - (python-shell-send-string string process msg) - (accept-process-output process) - (replace-regexp-in-string - (if (> (length python-shell-prompt-output-regexp) 0) - (format "\n*%s$\\|^%s\\|\n$" - python-shell-prompt-regexp - (or python-shell-prompt-output-regexp "")) - (format "\n*$\\|^%s\\|\n$" - python-shell-prompt-regexp)) - "" output-buffer))) + (let ((process (or process (python-shell-get-or-create-process))) + (comint-preoutput-filter-functions + '(python-shell-output-filter)) + (python-shell-output-filter-in-progress t) + (inhibit-quit t)) + (or + (with-local-quit + (python-shell-send-string string process msg) + (while python-shell-output-filter-in-progress + ;; `python-shell-output-filter' takes care of setting + ;; `python-shell-output-filter-in-progress' to NIL after it + ;; detects end of output. + (accept-process-output process)) + (prog1 + python-shell-output-filter-buffer + (setq python-shell-output-filter-buffer nil))) + (with-current-buffer (process-buffer process) + (comint-interrupt-subjob))))) (defun python-shell-internal-send-string (string) "Send STRING to the Internal Python interpreter. Returns the output. See `python-shell-send-string-no-output'." - (python-shell-send-string-no-output - ;; Makes this function compatible with the old - ;; python-send-receive. (At least for CEDET). - (replace-regexp-in-string "_emacs_out +" "" string) - (python-shell-internal-get-or-create-process) nil)) + ;; XXX Remove `python-shell-internal-last-output' once CEDET is + ;; updated to support this new mode. + (setq python-shell-internal-last-output + (python-shell-send-string-no-output + ;; Makes this function compatible with the old + ;; python-send-receive. (At least for CEDET). + (replace-regexp-in-string "_emacs_out +" "" string) + (python-shell-internal-get-or-create-process) nil))) (define-obsolete-function-alias - 'python-send-receive 'python-shell-internal-send-string "23.3") + 'python-send-receive 'python-shell-internal-send-string "24.3") (define-obsolete-function-alias - 'python-send-string 'python-shell-internal-send-string "23.3") + 'python-send-string 'python-shell-internal-send-string "24.3") (defun python-shell-send-region (start end) "Send the region delimited by START and END to inferior Python process." (interactive "r") - (python-shell-send-string (buffer-substring start end) nil t)) + (python-shell-send-string + (concat + (let ((line-num (line-number-at-pos start))) + ;; When sending a region, add blank lines for non sent code so + ;; backtraces remain correct. + (make-string (1- line-num) ?\n)) + (buffer-substring start end)) + nil t)) (defun python-shell-send-buffer (&optional arg) "Send the entire buffer to inferior Python process. - -With prefix arg include lines protected by \"if __name__ == '__main__':\"" +With prefix ARG allow execution of code inside blocks delimited +by \"if __name__== '__main__':\"" (interactive "P") (save-restriction (widen) - (python-shell-send-region - (point-min) - (or (and - (not arg) - (save-excursion - (re-search-forward (python-rx if-name-main) nil t)) - (match-beginning 0)) - (point-max))))) + (let ((str (buffer-substring (point-min) (point-max)))) + (and + (not arg) + (setq str (replace-regexp-in-string + (python-rx if-name-main) + "if __name__ == '__main__ ':" str))) + (python-shell-send-string str)))) (defun python-shell-send-defun (arg) "Send the current defun to inferior Python process. -When argument ARG is non-nil sends the innermost defun." +When argument ARG is non-nil do not include decorators." (interactive "P") (save-excursion (python-shell-send-region (progn - (or (python-beginning-of-defun-function) - (beginning-of-line)) + (end-of-line 1) + (while (and (or (python-nav-beginning-of-defun) + (beginning-of-line 1)) + (> (current-indentation) 0))) + (when (not arg) + (while (and (forward-line -1) + (looking-at (python-rx decorator)))) + (forward-line 1)) (point-marker)) (progn - (or (python-end-of-defun-function) - (end-of-line)) + (or (python-nav-end-of-defun) + (end-of-line 1)) (point-marker))))) (defun python-shell-send-file (file-name &optional process temp-file-name) @@ -1671,8 +2096,14 @@ FILE-NAME." (interactive "fFile to send: ") (let* ((process (or process (python-shell-get-or-create-process))) (temp-file-name (when temp-file-name - (expand-file-name temp-file-name))) - (file-name (or (expand-file-name file-name) temp-file-name))) + (expand-file-name + (or (file-remote-p temp-file-name 'localname) + temp-file-name)))) + (file-name (or (when file-name + (expand-file-name + (or (file-remote-p file-name 'localname) + file-name))) + temp-file-name))) (when (not file-name) (error "If FILE-NAME is nil then TEMP-FILE-NAME must be non-nil")) (python-shell-send-string @@ -1692,12 +2123,10 @@ FILE-NAME." "Send all setup code for shell. This function takes the list of setup code to send from the `python-shell-setup-codes' list." - (let ((msg "Sent %s") - (process (get-buffer-process (current-buffer)))) - (accept-process-output process python-shell-send-setup-max-wait) + (let ((process (get-buffer-process (current-buffer)))) (dolist (code python-shell-setup-codes) (when code - (message (format msg code)) + (message "Sent %s" code) (python-shell-send-string (symbol-value code) process))))) @@ -1758,27 +2187,71 @@ and use the following as the value of this variable: :type 'string :group 'python) -(defun python-shell-completion--get-completions (input process completion-code) - "Retrieve available completions for INPUT using PROCESS. -Argument COMPLETION-CODE is the python code used to get -completions on the current context." - (with-current-buffer (process-buffer process) - (let ((completions (python-shell-send-string-no-output - (format completion-code input) process))) - (when (> (length completions) 2) - (split-string completions "^'\\|^\"\\|;\\|'$\\|\"$" t))))) - -(defun python-shell-completion--do-completion-at-point (process) - "Do completion at point for PROCESS." - (with-syntax-table python-dotty-syntax-table - (let* ((beg - (save-excursion +(defun python-shell-completion-get-completions (process line input) + "Do completion at point for PROCESS. +LINE is used to detect the context on how to complete given +INPUT." + (let* ((prompt + ;; Get the last prompt for the inferior process + ;; buffer. This is used for the completion code selection + ;; heuristic. + (with-current-buffer (process-buffer process) + (buffer-substring-no-properties + (overlay-start comint-last-prompt-overlay) + (overlay-end comint-last-prompt-overlay)))) + (completion-context + ;; Check whether a prompt matches a pdb string, an import + ;; statement or just the standard prompt and use the + ;; correct python-shell-completion-*-code string + (cond ((and (> (length python-shell-completion-pdb-string-code) 0) + (string-match + (concat "^" python-shell-prompt-pdb-regexp) prompt)) + 'pdb) + ((and (> + (length python-shell-completion-module-string-code) 0) + (string-match + (concat "^" python-shell-prompt-regexp) prompt) + (string-match "^[ \t]*\\(from\\|import\\)[ \t]" line)) + 'import) + ((string-match + (concat "^" python-shell-prompt-regexp) prompt) + 'default) + (t nil))) + (completion-code + (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) + input))) + (and completion-code + (> (length input) 0) + (with-current-buffer (process-buffer process) + (let ((completions (python-shell-send-string-no-output + (format completion-code input) process))) + (and (> (length completions) 2) + (split-string completions + "^'\\|^\"\\|;\\|'$\\|\"$" t))))))) + +(defun python-shell-completion-complete-at-point (&optional process) + "Perform completion at point in inferior Python. +Optional argument PROCESS forces completions to be retrieved +using that one instead of current buffer's process." + (setq process (or process (get-buffer-process (current-buffer)))) + (let* ((start + (save-excursion + (with-syntax-table python-dotty-syntax-table (let* ((paren-depth (car (syntax-ppss))) (syntax-string "w_") (syntax-list (string-to-syntax syntax-string))) - ;; Stop scanning for the beginning of the completion subject - ;; after the char before point matches a delimiter - (while (member (car (syntax-after (1- (point)))) syntax-list) + ;; Stop scanning for the beginning of the completion + ;; subject after the char before point matches a + ;; delimiter + (while (member + (car (syntax-after (1- (point)))) syntax-list) (skip-syntax-backward syntax-string) (when (or (equal (char-before) ?\)) (equal (char-before) ?\")) @@ -1786,60 +2259,16 @@ completions on the current context." (while (or ;; honor initial paren depth (> (car (syntax-ppss)) paren-depth) - (python-info-ppss-context 'string)) - (forward-char -1)))) - (point))) - (end (point)) - (line (buffer-substring-no-properties (point-at-bol) end)) - (input (buffer-substring-no-properties beg end)) - ;; Get the last prompt for the inferior process buffer. This is - ;; used for the completion code selection heuristic. - (prompt - (with-current-buffer (process-buffer process) - (buffer-substring-no-properties - (overlay-start comint-last-prompt-overlay) - (overlay-end comint-last-prompt-overlay)))) - (completion-context - ;; Check wether a prompt matches a pdb string, an import statement - ;; or just the standard prompt and use the correct - ;; python-shell-completion-*-code string - (cond ((and (> (length python-shell-completion-pdb-string-code) 0) - (string-match - (concat "^" python-shell-prompt-pdb-regexp) prompt)) - 'pdb) - ((and (> - (length python-shell-completion-module-string-code) 0) - (string-match - (concat "^" python-shell-prompt-regexp) prompt) - (string-match "^[ \t]*\\(from\\|import\\)[ \t]" line)) - 'import) - ((string-match - (concat "^" python-shell-prompt-regexp) prompt) - '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))) - (input - (if (eq completion-context 'import) - (replace-regexp-in-string "^[ \t]+" "" line) - input)) - (completions - (and completion-code (> (length input) 0) - (python-shell-completion--get-completions - input process completion-code)))) - (list beg end completions)))) - -(defun python-shell-completion-complete-at-point () - "Perform completion at point in inferior Python process." - (interactive) - (and comint-last-prompt-overlay - (> (point-marker) (overlay-end comint-last-prompt-overlay)) - (python-shell-completion--do-completion-at-point - (get-buffer-process (current-buffer))))) + (python-syntax-context 'string)) + (forward-char -1))) + (point))))) + (end (point))) + (list start end + (completion-table-dynamic + (apply-partially + #'python-shell-completion-get-completions + process (buffer-substring-no-properties + (line-beginning-position) end)))))) (defun python-shell-completion-complete-or-indent () "Complete or indent depending on the context. @@ -1873,11 +2302,9 @@ Used to extract the current line and module being inspected." "Variable containing the value of the current tracked buffer. Never set this variable directly, use `python-pdbtrack-set-tracked-buffer' instead.") -(make-variable-buffer-local 'python-pdbtrack-tracked-buffer) (defvar python-pdbtrack-buffers-to-kill nil "List of buffers to be deleted after tracking finishes.") -(make-variable-buffer-local 'python-pdbtrack-buffers-to-kill) (defun python-pdbtrack-set-tracked-buffer (file-name) "Set the buffer for FILE-NAME as the tracked buffer. @@ -1901,15 +2328,17 @@ Argument OUTPUT is a string with the output from the comint process." (file-name (with-temp-buffer (insert full-output) - (goto-char (point-min)) - ;; OK, this sucked but now it became a cool hack. The - ;; stacktrace information normally is on the first line - ;; but in some cases (like when doing a step-in) it is - ;; on the second. - (when (or (looking-at python-pdbtrack-stacktrace-info-regexp) - (and - (forward-line) - (looking-at python-pdbtrack-stacktrace-info-regexp))) + ;; When the debugger encounters a pdb.set_trace() + ;; command, it prints a single stack frame. Sometimes + ;; it prints a bit of extra information about the + ;; arguments of the present function. When ipdb + ;; 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 corresponding + ;; file/line number. + (goto-char (point-max)) + (when (re-search-backward python-pdbtrack-stacktrace-info-regexp nil t) (setq line-number (string-to-number (match-string-no-properties 2))) (match-string-no-properties 1))))) @@ -1950,11 +2379,10 @@ Argument OUTPUT is a string with the output from the comint process." For this to work the best as possible you should call `python-shell-send-buffer' from time to time so context in inferior python process is updated properly." - (interactive) (let ((process (python-shell-get-process))) (if (not process) - (error "Completion needs an inferior Python process running.") - (python-shell-completion--do-completion-at-point process)))) + (error "Completion needs an inferior Python process running") + (python-shell-completion-complete-at-point process)))) (add-to-list 'debug-ignored-errors "^Completion needs an inferior Python process running.") @@ -1964,37 +2392,105 @@ inferior python process is updated properly." (defcustom python-fill-comment-function 'python-fill-comment "Function to fill comments. -This is the function used by `python-fill-paragraph-function' to +This is the function used by `python-fill-paragraph' to fill comments." :type 'symbol - :group 'python - :safe 'symbolp) + :group 'python) (defcustom python-fill-string-function 'python-fill-string "Function to fill strings. -This is the function used by `python-fill-paragraph-function' to +This is the function used by `python-fill-paragraph' to fill strings." :type 'symbol - :group 'python - :safe 'symbolp) + :group 'python) (defcustom python-fill-decorator-function 'python-fill-decorator "Function to fill decorators. -This is the function used by `python-fill-paragraph-function' to +This is the function used by `python-fill-paragraph' to fill decorators." :type 'symbol - :group 'python - :safe 'symbolp) + :group 'python) (defcustom python-fill-paren-function 'python-fill-paren "Function to fill parens. -This is the function used by `python-fill-paragraph-function' to +This is the function used by `python-fill-paragraph' to fill parens." :type 'symbol + :group 'python) + +(defcustom python-fill-docstring-style 'pep-257 + "Style used to fill docstrings. +This affects `python-fill-string' behavior with regards to +triple quotes positioning. + +Possible values are DJANGO, ONETWO, PEP-257, PEP-257-NN, +SYMMETRIC, and NIL. A value of NIL won't care about quotes +position and will treat docstrings a normal string, any other +value may result in one of the following docstring styles: + +DJANGO: + + \"\"\" + Process foo, return bar. + \"\"\" + + \"\"\" + Process foo, return bar. + + If processing fails throw ProcessingError. + \"\"\" + +ONETWO: + + \"\"\"Process foo, return bar.\"\"\" + + \"\"\" + Process foo, return bar. + + If processing fails throw ProcessingError. + + \"\"\" + +PEP-257: + + \"\"\"Process foo, return bar.\"\"\" + + \"\"\"Process foo, return bar. + + If processing fails throw ProcessingError. + + \"\"\" + +PEP-257-NN: + + \"\"\"Process foo, return bar.\"\"\" + + \"\"\"Process foo, return bar. + + If processing fails throw ProcessingError. + \"\"\" + +SYMMETRIC: + + \"\"\"Process foo, return bar.\"\"\" + + \"\"\" + Process foo, return bar. + + If processing fails throw ProcessingError. + \"\"\"" + :type '(choice + (const :tag "Don't format docstrings" nil) + (const :tag "Django's coding standards style." django) + (const :tag "One newline and start and Two at end style." onetwo) + (const :tag "PEP-257 with 2 newlines at end of string." pep-257) + (const :tag "PEP-257 with 1 newline at end of string." pep-257-nn) + (const :tag "Symmetric style." symmetric)) :group 'python - :safe 'symbolp) + :safe (lambda (val) + (memq val '(django onetwo pep-257 pep-257-nn symmetric nil)))) -(defun python-fill-paragraph-function (&optional justify) +(defun python-fill-paragraph (&optional justify) "`fill-paragraph-function' handling multi-line strings and possibly comments. If any of the current line is in or at the end of a multi-line string, fill the string or the paragraph of it that point is in, preserving @@ -2002,21 +2498,21 @@ the string's indentation. Optional argument JUSTIFY defines if the paragraph should be justified." (interactive "P") (save-excursion - (back-to-indentation) (cond ;; Comments - ((funcall python-fill-comment-function justify)) + ((python-syntax-context 'comment) + (funcall python-fill-comment-function justify)) ;; Strings/Docstrings - ((save-excursion (skip-chars-forward "\"'uUrR") - (python-info-ppss-context 'string)) + ((save-excursion (or (python-syntax-context 'string) + (equal (string-to-syntax "|") + (syntax-after (point))))) (funcall python-fill-string-function justify)) ;; Decorators ((equal (char-after (save-excursion - (back-to-indentation) - (point-marker))) ?@) + (python-nav-beginning-of-statement))) ?@) (funcall python-fill-decorator-function justify)) ;; Parens - ((or (python-info-ppss-context 'paren) + ((or (python-syntax-context 'paren) (looking-at (python-rx open-paren)) (save-excursion (skip-syntax-forward "^(" (line-end-position)) @@ -2025,71 +2521,100 @@ Optional argument JUSTIFY defines if the paragraph should be justified." (t t)))) (defun python-fill-comment (&optional justify) - "Comment fill function for `python-fill-paragraph-function'. + "Comment fill function for `python-fill-paragraph'. JUSTIFY should be used (if applicable) as in `fill-paragraph'." (fill-comment-paragraph justify)) (defun python-fill-string (&optional justify) - "String fill function for `python-fill-paragraph-function'. + "String fill function for `python-fill-paragraph'. JUSTIFY should be used (if applicable) as in `fill-paragraph'." - (let ((marker (point-marker)) - (string-start-marker - (progn - (skip-chars-forward "\"'uUrR") - (goto-char (python-info-ppss-context 'string)) - (skip-chars-forward "\"'uUrR") - (point-marker))) - (reg-start (line-beginning-position)) - (string-end-marker - (progn - (while (python-info-ppss-context 'string) - (goto-char (1+ (point-marker)))) - (skip-chars-backward "\"'") - (point-marker))) - (reg-end (line-end-position)) - (fill-paragraph-function)) + (let* ((marker (point-marker)) + (str-start-pos + (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 + (save-excursion + (goto-char (+ str-start-pos num-quotes)) + (or (re-search-forward (rx (syntax string-delimiter)) nil t) + (goto-char (point-max))) + (point-marker))) + (multi-line-p + ;; Docstring styles may vary for oneliners and multi-liners. + (> (count-matches "\n" str-start-pos str-end-pos) 0)) + (delimiters-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))))) + (docstring-p (save-excursion + ;; Consider docstrings those strings which + ;; start on a line by themselves. + (python-nav-beginning-of-statement) + (and (= (point) str-start-pos)))) + (fill-paragraph-function)) (save-restriction - (narrow-to-region reg-start reg-end) - (save-excursion - (goto-char string-start-marker) - (delete-region (point-marker) (progn - (skip-syntax-forward "> ") - (point-marker))) - (goto-char string-end-marker) - (delete-region (point-marker) (progn - (skip-syntax-backward "> ") - (point-marker))) - (save-excursion - (goto-char marker) - (fill-paragraph justify)) - ;; If there is a newline in the docstring lets put triple - ;; quote in it's own line to follow pep 8 - (when (save-excursion - (re-search-backward "\n" string-start-marker t)) - (newline) - (newline-and-indent)) - (fill-paragraph justify)))) t) + (narrow-to-region str-start-pos str-end-pos) + (fill-paragraph justify)) + (save-excursion + (when (and docstring-p python-fill-docstring-style) + ;; Add the number of newlines indicated by the selected style + ;; at the start of the docstring. + (goto-char (+ str-start-pos num-quotes)) + (delete-region (point) (progn + (skip-syntax-forward "> ") + (point))) + (and (car delimiters-style) + (or (newline (car delimiters-style)) t) + ;; Indent only if a newline is added. + (indent-according-to-mode)) + ;; Add the number of newlines indicated by the selected style + ;; at the end of the docstring. + (goto-char (if (not (= str-end-pos (point-max))) + (- str-end-pos num-quotes) + str-end-pos)) + (delete-region (point) (progn + (skip-syntax-backward "> ") + (point))) + (and (cdr delimiters-style) + ;; Add newlines only if string ends. + (not (= str-end-pos (point-max))) + (or (newline (cdr delimiters-style)) t) + ;; Again indent only if a newline is added. + (indent-according-to-mode))))) t) (defun python-fill-decorator (&optional justify) - "Decorator fill function for `python-fill-paragraph-function'. + "Decorator fill function for `python-fill-paragraph'. JUSTIFY should be used (if applicable) as in `fill-paragraph'." t) (defun python-fill-paren (&optional justify) - "Paren fill function for `python-fill-paragraph-function'. + "Paren fill function for `python-fill-paragraph'. JUSTIFY should be used (if applicable) as in `fill-paragraph'." (save-restriction (narrow-to-region (progn - (while (python-info-ppss-context 'paren) + (while (python-syntax-context 'paren) (goto-char (1- (point-marker)))) (point-marker) (line-beginning-position)) (progn - (when (not (python-info-ppss-context 'paren)) + (when (not (python-syntax-context 'paren)) (end-of-line) - (when (not (python-info-ppss-context 'paren)) + (when (not (python-syntax-context 'paren)) (skip-syntax-backward "^)"))) - (while (python-info-ppss-context 'paren) + (while (python-syntax-context 'paren) (goto-char (1+ (point-marker)))) (point-marker))) (let ((paragraph-start "\f\\|[ \t]*$") @@ -2113,6 +2638,9 @@ the if condition." :group 'python :safe 'booleanp) +(define-obsolete-variable-alias + 'python-use-skeletons 'python-skeleton-autoinsert "24.3") + (defvar python-skeleton-available '() "Internal list of available skeletons.") @@ -2124,29 +2652,30 @@ the if condition." ;; Only expand in code. :enable-function (lambda () (and - (not (or (python-info-ppss-context 'string) - (python-info-ppss-context 'comment))) + (not (python-syntax-comment-or-string-p)) python-skeleton-autoinsert))) (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'." + (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) + (define-abbrev python-mode-abbrev-table ,name "" ',function-name + :system t) (setq python-skeleton-available (cons ',function-name python-skeleton-available)) (define-skeleton ,function-name ,(or doc (format "Insert %s statement." name)) ,@skel)))) -(put 'python-skeleton-define 'lisp-indent-function 2) (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." + (declare (indent 2)) (let* ((name (symbol-name name)) (function-name (intern (concat "python-skeleton--" name))) (msg (format @@ -2162,7 +2691,6 @@ The skeleton will be bound to python-skeleton-NAME." (unless (y-or-n-p ,msg) (signal 'quit t)) ,@skel))) -(put 'python-define-auxiliary-skeleton 'lisp-indent-function 2) (python-define-auxiliary-skeleton else nil) @@ -2207,17 +2735,17 @@ The skeleton will be bound to python-skeleton-NAME." (python-skeleton-define def nil "Function name: " - "def " str " (" ("Parameter, %s: " - (unless (equal ?\( (char-before)) ", ") - str) "):" \n - "\"\"\"" - "\"\"\"" \n - > _ \n) + "def " str "(" ("Parameter, %s: " + (unless (equal ?\( (char-before)) ", ") + str) "):" \n + "\"\"\"" - "\"\"\"" \n + > _ \n) (python-skeleton-define class nil "Class name: " - "class " str " (" ("Inheritance, %s: " - (unless (equal ?\( (char-before)) ", ") - str) + "class " str "(" ("Inheritance, %s: " + (unless (equal ?\( (char-before)) ", ") + str) & ")" | -2 ":" \n "\"\"\"" - "\"\"\"" \n @@ -2231,7 +2759,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 @@ -2361,46 +2889,19 @@ Runs COMMAND, a shell command, as if by `compile'. See (defun python-eldoc--get-doc-at-point (&optional force-input force-process) "Internal implementation to get documentation at point. -If not FORCE-INPUT is passed then what `current-word' returns -will be used. If not FORCE-PROCESS is passed what -`python-shell-get-process' returns is used." +If not FORCE-INPUT is passed then what +`python-info-current-symbol' returns will be used. If not +FORCE-PROCESS is passed what `python-shell-get-process' returns +is used." (let ((process (or force-process (python-shell-get-process)))) (if (not process) - "Eldoc needs an inferior Python process running." - (let* ((current-defun (python-info-current-defun)) - (input (or force-input - (with-syntax-table python-dotty-syntax-table - (if (not current-defun) - (current-word) - (concat current-defun "." (current-word)))))) - (ppss (syntax-ppss)) - (help (when (and - input - (not (string= input (concat current-defun "."))) - (not (or (python-info-ppss-context 'string ppss) - (python-info-ppss-context 'comment ppss)))) - (when (string-match - (concat - (regexp-quote (concat current-defun ".")) - "self\\.") input) - (with-temp-buffer - (insert input) - (goto-char (point-min)) - (forward-word) - (forward-char) - (delete-region - (point-marker) (search-forward "self.")) - (setq input (buffer-substring - (point-min) (point-max))))) - (python-shell-send-string-no-output - (format python-eldoc-string-code input) process)))) - (with-current-buffer (process-buffer process) - (when comint-last-prompt-overlay - (delete-region comint-last-input-end - (overlay-start comint-last-prompt-overlay)))) - (when (and help - (not (string= help "\n"))) - help))))) + (error "Eldoc needs an inferior Python process running") + (let ((input (or force-input + (python-info-current-symbol t)))) + (and input + (python-shell-send-string-no-output + (format python-eldoc-string-code input) + process)))))) (defun python-eldoc-function () "`eldoc-documentation-function' for Python. @@ -2413,130 +2914,29 @@ inferior python process is updated properly." "Get help on SYMBOL using `help'. Interactively, prompt for symbol." (interactive - (let ((symbol (with-syntax-table python-dotty-syntax-table - (current-word))) + (let ((symbol (python-info-current-symbol t)) (enable-recursive-minibuffers t)) (list (read-string (if symbol (format "Describe symbol (default %s): " symbol) "Describe symbol: ") nil nil symbol)))) - (let ((process (python-shell-get-process))) - (if (not process) - (message "Eldoc needs an inferior Python process running.") - (message (python-eldoc--get-doc-at-point symbol process))))) + (message (python-eldoc--get-doc-at-point symbol))) + +(add-to-list 'debug-ignored-errors + "^Eldoc needs an inferior Python process running.") ;;; Imenu -(defcustom python-imenu-include-defun-type t - "Non-nil make imenu items to include its type." - :type 'boolean - :group 'python - :safe 'booleanp) - -(defcustom python-imenu-make-tree t - "Non-nil make imenu to build a tree menu. -Set to nil for speed." - :type 'boolean - :group 'python - :safe 'booleanp) - -(defcustom python-imenu-subtree-root-label "" - "Label displayed to navigate to root from a subtree. -It can contain a \"%s\" which will be replaced with the root name." - :type 'string - :group 'python - :safe 'stringp) - -(defvar python-imenu-index-alist nil - "Calculated index tree for imenu.") - -(defun python-imenu-tree-assoc (keylist tree) - "Using KEYLIST traverse TREE." - (if keylist - (python-imenu-tree-assoc (cdr keylist) - (ignore-errors (assoc (car keylist) tree))) - tree)) - -(defun python-imenu-make-element-tree (element-list full-element plain-index) - "Make a tree from plain alist of module names. -ELEMENT-LIST is the defun name splitted by \".\" and FULL-ELEMENT -is the same thing, the difference is that FULL-ELEMENT remains -untouched in all recursive calls. -Argument PLAIN-INDEX is the calculated plain index used to build the tree." - (when (not (python-imenu-tree-assoc full-element python-imenu-index-alist)) - (when element-list - (let* ((subelement-point (cdr (assoc - (mapconcat #'identity full-element ".") - plain-index))) - (subelement-name (car element-list)) - (subelement-position (python-util-position - subelement-name full-element)) - (subelement-path (when subelement-position - (butlast - full-element - (- (length full-element) - subelement-position))))) - (let ((path-ref (python-imenu-tree-assoc subelement-path - python-imenu-index-alist))) - (if (not path-ref) - (push (cons subelement-name subelement-point) - python-imenu-index-alist) - (when (not (listp (cdr path-ref))) - ;; Modifiy root cdr to be a list - (setcdr path-ref - (list (cons (format python-imenu-subtree-root-label - (car path-ref)) - (cdr (assoc - (mapconcat #'identity - subelement-path ".") - plain-index)))))) - (when (not (assoc subelement-name path-ref)) - (push (cons subelement-name subelement-point) (cdr path-ref)))))) - (python-imenu-make-element-tree (cdr element-list) - full-element plain-index)))) - -(defun python-imenu-make-tree (index) - "Build the imenu alist tree from plain INDEX. - -The idea of this function is that given the alist: - - '((\"Test\" . 100) - (\"Test.__init__\" . 200) - (\"Test.some_method\" . 300) - (\"Test.some_method.another\" . 400) - (\"Test.something_else\" . 500) - (\"test\" . 600) - (\"test.reprint\" . 700) - (\"test.reprint\" . 800)) - -This tree gets built: - - '((\"Test\" . ((\"jump to...\" . 100) - (\"__init__\" . 200) - (\"some_method\" . ((\"jump to...\" . 300) - (\"another\" . 400))) - (\"something_else\" . 500))) - (\"test\" . ((\"jump to...\" . 600) - (\"reprint\" . 700) - (\"reprint\" . 800)))) - -Internally it uses `python-imenu-make-element-tree' to create all -branches for each element." - (setq python-imenu-index-alist nil) - (mapc (lambda (element) - (python-imenu-make-element-tree element element index)) - (mapcar (lambda (element) - (split-string (car element) "\\." t)) index)) - python-imenu-index-alist) - -(defun python-imenu-create-index () - "`imenu-create-index-function' for Python." - (let ((index - (python-nav-list-defun-positions python-imenu-include-defun-type))) - (if python-imenu-make-tree - (python-imenu-make-tree index) - index))) +(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)))) ;;; Misc helpers @@ -2547,30 +2947,122 @@ Optional argument INCLUDE-TYPE indicates to include the type of the defun. This function is compatible to be used as `add-log-current-defun-function' since it returns nil if point is not inside a defun." - (let ((names '()) - (min-indent) - (first-run t)) - (save-restriction - (widen) - (save-excursion - (goto-char (line-end-position)) - (python-util-forward-comment -1) - (setq min-indent (current-indentation)) - (while (python-beginning-of-defun-function 1 t) - (when (or (< (current-indentation) min-indent) - first-run) - (setq first-run nil) - (setq min-indent (current-indentation)) - (looking-at python-nav-beginning-of-defun-regexp) - (setq names (cons - (if (not include-type) - (match-string-no-properties 1) - (mapconcat 'identity - (split-string - (match-string-no-properties 0)) " ")) - names)))))) - (when names - (mapconcat (lambda (string) string) names ".")))) + (save-restriction + (widen) + (save-excursion + (end-of-line 1) + (let ((names) + (starting-indentation (current-indentation)) + (starting-pos (point)) + (first-run t) + (last-indent) + (type)) + (catch 'exit + (while (python-nav-beginning-of-defun 1) + (when (and + (or (not last-indent) + (< (current-indentation) last-indent)) + (or + (and first-run + (save-excursion + ;; If this is the first run, we may add + ;; the current defun at point. + (setq first-run nil) + (goto-char starting-pos) + (python-nav-beginning-of-statement) + (beginning-of-line 1) + (looking-at-p + python-nav-beginning-of-defun-regexp))) + (< starting-pos + (save-excursion + (let ((min-indent + (+ (current-indentation) + python-indent-offset))) + (if (< starting-indentation min-indent) + ;; If the starting indentation is not + ;; within the min defun indent make the + ;; check fail. + starting-pos + ;; Else go to the end of defun and add + ;; up the current indentation to the + ;; ending position. + (python-nav-end-of-defun) + (+ (point) + (if (>= (current-indentation) min-indent) + (1+ (current-indentation)) + 0)))))))) + (setq last-indent (current-indentation)) + (if (or (not include-type) type) + (setq names (cons (match-string-no-properties 1) names)) + (let ((match (split-string (match-string-no-properties 0)))) + (setq type (car match)) + (setq names (cons (cadr match) names))))) + ;; Stop searching ASAP. + (and (= (current-indentation) 0) (throw 'exit t)))) + (and names + (concat (and type (format "%s " type)) + (mapconcat 'identity names "."))))))) + +(defun python-info-current-symbol (&optional replace-self) + "Return current symbol using dotty syntax. +With optional argument REPLACE-SELF convert \"self\" to current +parent defun name." + (let ((name + (and (not (python-syntax-comment-or-string-p)) + (with-syntax-table python-dotty-syntax-table + (let ((sym (symbol-at-point))) + (and sym + (substring-no-properties (symbol-name sym)))))))) + (when name + (if (not replace-self) + name + (let ((current-defun (python-info-current-defun))) + (if (not current-defun) + name + (replace-regexp-in-string + (python-rx line-start word-start "self" word-end ?.) + (concat + (mapconcat 'identity + (butlast (split-string current-defun "\\.")) + ".") ".") + name))))))) + +(defun python-info-statement-starts-block-p () + "Return non-nil if current statement opens a block." + (save-excursion + (python-nav-beginning-of-statement) + (looking-at (python-rx block-start)))) + +(defun python-info-statement-ends-block-p () + "Return non-nil if point is at end of block." + (let ((end-of-block-pos (save-excursion + (python-nav-end-of-block))) + (end-of-statement-pos (save-excursion + (python-nav-end-of-statement)))) + (and end-of-block-pos end-of-statement-pos + (= end-of-block-pos end-of-statement-pos)))) + +(defun python-info-beginning-of-statement-p () + "Return non-nil if point is at beginning of statement." + (= (point) (save-excursion + (python-nav-beginning-of-statement) + (point)))) + +(defun python-info-end-of-statement-p () + "Return non-nil if point is at end of statement." + (= (point) (save-excursion + (python-nav-end-of-statement) + (point)))) + +(defun python-info-beginning-of-block-p () + "Return non-nil if point is at beginning of block." + (and (python-info-beginning-of-statement-p) + (python-info-statement-starts-block-p))) + +(defun python-info-end-of-block-p () + "Return non-nil if point is at end of block." + (and (python-info-end-of-statement-p) + (python-info-statement-ends-block-p))) (defun python-info-closing-block () "Return the point of the block the current line closes." @@ -2622,26 +3114,27 @@ With optional argument LINE-NUMBER, check that line instead." (save-restriction (widen) (when line-number - (goto-char line-number)) + (python-util-goto-line line-number)) (while (and (not (eobp)) (goto-char (line-end-position)) - (python-info-ppss-context 'paren) + (python-syntax-context 'paren) (not (equal (char-before (point)) ?\\))) (forward-line 1)) (when (equal (char-before) ?\\) (point-marker))))) -(defun python-info-beginning-of-backlash (&optional line-number) - "Return the point where the backlashed line starts." +(defun python-info-beginning-of-backslash (&optional line-number) + "Return the point where the backslashed line start. +Optional argument LINE-NUMBER forces the line number to check against." (save-excursion (save-restriction (widen) (when line-number - (goto-char line-number)) + (python-util-goto-line line-number)) (when (python-info-line-ends-backslash-p) (while (save-excursion (goto-char (line-beginning-position)) - (python-info-ppss-context 'paren)) + (python-syntax-context 'paren)) (forward-line -1)) (back-to-indentation) (point-marker))))) @@ -2655,31 +3148,27 @@ where the continued line ends." (widen) (let* ((context-type (progn (back-to-indentation) - (python-info-ppss-context-type))) + (python-syntax-context-type))) (line-start (line-number-at-pos)) (context-start (when context-type - (python-info-ppss-context context-type)))) + (python-syntax-context context-type)))) (cond ((equal context-type 'paren) ;; Lines inside a paren are always a continuation line ;; (except the first one). - (when (equal (python-info-ppss-context-type) 'paren) - (python-util-forward-comment -1) - (python-util-forward-comment -1) - (point-marker))) - ((or (equal context-type 'comment) - (equal context-type 'string)) + (python-util-forward-comment -1) + (point-marker)) + ((member context-type '(string comment)) ;; move forward an roll again (goto-char context-start) (python-util-forward-comment) (python-info-continuation-line-p)) (t - ;; Not within a paren, string or comment, the only way we are - ;; dealing with a continuation line is that previous line - ;; contains a backslash, and this can only be the previous line - ;; from current + ;; Not within a paren, string or comment, the only way + ;; we are dealing with a continuation line is that + ;; previous line contains a backslash, and this can + ;; only be the previous line from current (back-to-indentation) (python-util-forward-comment -1) - (python-util-forward-comment -1) (when (and (equal (1- line-start) (line-number-at-pos)) (python-info-line-ends-backslash-p)) (point-marker)))))))) @@ -2707,50 +3196,40 @@ operator." assignment-operator not-simple-operator) (line-end-position) t) - (not (or (python-info-ppss-context 'string) - (python-info-ppss-context 'paren) - (python-info-ppss-context 'comment))))) + (not (python-syntax-context-type)))) (skip-syntax-forward "\s") (point-marker))))) -(defun python-info-ppss-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." - (let ((ppss (or syntax-ppss (syntax-ppss)))) - (case type - ('comment - (and (nth 4 ppss) - (nth 8 ppss))) - ('string - (nth 8 ppss)) - ('paren - (nth 1 ppss)) - (t nil)))) - -(defun python-info-ppss-context-type (&optional syntax-ppss) - "Return the context type using SYNTAX-PPSS. -The type returned can be 'comment, 'string or 'paren." - (let ((ppss (or syntax-ppss (syntax-ppss)))) - (cond - ((and (nth 4 ppss) - (nth 8 ppss)) - 'comment) - ((nth 8 ppss) - 'string) - ((nth 1 ppss) - 'paren) - (t nil)))) +(defun python-info-looking-at-beginning-of-defun (&optional syntax-ppss) + "Check if point is at `beginning-of-defun' using SYNTAX-PPSS." + (and (not (python-syntax-context-type (or syntax-ppss (syntax-ppss)))) + (save-excursion + (beginning-of-line 1) + (looking-at python-nav-beginning-of-defun-regexp)))) + +(defun python-info-current-line-comment-p () + "Check if current line is a comment line." + (char-equal + (or (char-after (+ (line-beginning-position) (current-indentation))) ?_) + ?#)) + +(defun python-info-current-line-empty-p () + "Check if current line is empty, ignoring whitespace." + (save-excursion + (beginning-of-line 1) + (looking-at + (python-rx line-start (* whitespace) + (group (* not-newline)) + (* whitespace) line-end)) + (string-equal "" (match-string-no-properties 1)))) ;;; Utility functions -(defun python-util-position (item seq) - "Find the first occurrence of ITEM in SEQ. -Return the index of the matching item, or nil if not found." - (let ((member-result (member item seq))) - (when member-result - (- (length seq) (length member-result))))) +(defun python-util-goto-line (line-number) + "Move point to LINE-NUMBER." + (goto-char (point-min)) + (forward-line (1- line-number))) ;; Stolen from org-mode (defun python-util-clone-local-variables (from-buffer &optional regexp) @@ -2767,8 +3246,9 @@ to \"^python-\"." (buffer-local-variables from-buffer))) (defun python-util-forward-comment (&optional direction) - "Python mode specific version of `forward-comment'." - (let ((comment-start (python-info-ppss-context 'comment)) + "Python mode specific version of `forward-comment'. +Optional argument DIRECTION defines the direction to move to." + (let ((comment-start (python-syntax-context 'comment)) (factor (if (< (or direction 0) 0) -99999 99999))) @@ -2778,7 +3258,7 @@ to \"^python-\"." ;;;###autoload -(define-derived-mode python-mode fundamental-mode "Python" +(define-derived-mode python-mode prog-mode "Python" "Major mode for editing Python files. \\{python-mode-map} @@ -2793,10 +3273,14 @@ if that value is non-nil." (set (make-local-variable 'parse-sexp-lookup-properties) t) (set (make-local-variable 'parse-sexp-ignore-comments) t) + (set (make-local-variable 'forward-sexp-function) + 'python-nav-forward-sexp) + (set (make-local-variable 'font-lock-defaults) - '(python-font-lock-keywords - nil nil nil nil - (font-lock-syntactic-keywords . python-font-lock-syntactic-keywords))) + '(python-font-lock-keywords nil nil nil nil)) + + (set (make-local-variable 'syntax-propertize-function) + python-syntax-propertize-function) (set (make-local-variable 'indent-line-function) #'python-indent-line-function) @@ -2804,12 +3288,12 @@ if that value is non-nil." (set (make-local-variable 'paragraph-start) "\\s-*$") (set (make-local-variable 'fill-paragraph-function) - 'python-fill-paragraph-function) + 'python-fill-paragraph) (set (make-local-variable 'beginning-of-defun-function) - #'python-beginning-of-defun-function) + #'python-nav-beginning-of-defun) (set (make-local-variable 'end-of-defun-function) - #'python-end-of-defun-function) + #'python-nav-end-of-defun) (add-hook 'completion-at-point-functions 'python-completion-complete-at-point nil 'local) @@ -2817,11 +3301,17 @@ if that value is non-nil." (add-hook 'post-self-insert-hook 'python-indent-post-self-insert-function nil 'local) - (setq imenu-create-index-function #'python-imenu-create-index) + (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 'add-log-current-defun-function) #'python-info-current-defun) + (add-hook 'which-func-functions #'python-info-current-defun nil t) + (set (make-local-variable 'skeleton-further-elements) '((abbrev-mode nil) (< '(backward-delete-char-untabify (min python-indent-offset @@ -2834,7 +3324,7 @@ if that value is non-nil." (add-to-list 'hs-special-modes-alist `(python-mode "^\\s-*\\(?:def\\|class\\)\\>" nil "#" ,(lambda (arg) - (python-end-of-defun-function)) nil)) + (python-nav-end-of-defun)) nil)) (set (make-local-variable 'mode-require-final-newline) t) @@ -2848,9 +3338,17 @@ if that value is non-nil." (python-skeleton-add-menu-items) + (make-local-variable 'python-shell-internal-buffer) + (when python-indent-guess-indent-offset (python-indent-guess-indent-offset))) (provide 'python) + +;; Local Variables: +;; coding: utf-8 +;; indent-tabs-mode: nil +;; End: + ;;; python.el ends here