diff options
Diffstat (limited to 'lisp/progmodes/python.el')
-rw-r--r-- | lisp/progmodes/python.el | 367 |
1 files changed, 252 insertions, 115 deletions
diff --git a/lisp/progmodes/python.el b/lisp/progmodes/python.el index 67b44aa1bbe..eb17d075921 100644 --- a/lisp/progmodes/python.el +++ b/lisp/progmodes/python.el @@ -482,19 +482,10 @@ The type returned can be `comment', `string' or `paren'." 'python-info-ppss-comment-or-string-p #'python-syntax-comment-or-string-p "24.3") -(defun python-docstring-at-p (pos) - "Check to see if there is a docstring at POS." - (save-excursion - (goto-char pos) - (if (looking-at-p "'''\\|\"\"\"") - (progn - (python-nav-backward-statement) - (looking-at "\\`\\|class \\|def ")) - nil))) - (defun python-font-lock-syntactic-face-function (state) + "Return syntactic face given STATE." (if (nth 3 state) - (if (python-docstring-at-p (nth 8 state)) + (if (python-info-docstring-p state) font-lock-doc-face font-lock-string-face) font-lock-comment-face)) @@ -853,7 +844,9 @@ keyword ;; Inside a string. ((let ((start (python-syntax-context 'string ppss))) (when start - (cons :inside-string start)))) + (cons (if (python-info-docstring-p) + :inside-docstring + :inside-string) start)))) ;; Inside a paren. ((let* ((start (python-syntax-context 'paren ppss)) (starts-in-newline @@ -998,6 +991,12 @@ possibilities can be narrowed to specific indentation points." ;; Copy previous indentation. (goto-char start) (current-indentation)) + (`(:inside-docstring . ,start) + (let* ((line-indentation (current-indentation)) + (base-indent (progn + (goto-char start) + (current-indentation)))) + (max line-indentation base-indent))) (`(,(or :after-block-start :after-backslash-first-line :inside-paren-newline-start) . ,start) @@ -1147,14 +1146,15 @@ Called from a program, START and END specify the region to indent." (not line-is-comment-p)) (python-info-current-line-empty-p))))) ;; Don't mess with strings, unless it's the - ;; enclosing set of quotes. + ;; enclosing set of quotes or a docstring. (or (not (python-syntax-context 'string)) (eq (syntax-after (+ (1- (point)) (current-indentation) (python-syntax-count-quotes (char-after) (point)))) - (string-to-syntax "|"))) + (string-to-syntax "|")) + (python-info-docstring-p)) ;; Skip if current line is a block start, a ;; dedenter or block ender. (save-excursion @@ -1580,11 +1580,13 @@ forward only one sexp, else move backwards." (while (and (funcall search-fn paren-regexp nil t) (python-syntax-context 'paren))))))) -(defun python-nav--forward-sexp (&optional dir safe) +(defun python-nav--forward-sexp (&optional dir safe skip-parens-p) "Move to forward sexp. With positive optional argument DIR direction move forward, else backwards. When optional argument SAFE is non-nil do not throw -errors when at end of sexp, skip it instead." +errors when at end of sexp, skip it instead. With optional +argument SKIP-PARENS-P force sexp motion to ignore parenthesized +expressions when looking at them in either direction." (setq dir (or dir 1)) (unless (= dir 0) (let* ((forward-p (if (> dir 0) @@ -1596,11 +1598,13 @@ errors when at end of sexp, skip it instead." ;; 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 ")"))))) + ((and (not skip-parens-p) + (or (eq context-type 'paren) + (if forward-p + (eq (syntax-class (syntax-after (point))) + (car (string-to-syntax "("))) + (eq (syntax-class (syntax-after (1- (point)))) + (car (string-to-syntax ")")))))) ;; Inside a paren or looking at it, lisp knows what to do. (if safe (python-nav--lisp-forward-sexp-safe dir) @@ -1636,7 +1640,7 @@ errors when at end of sexp, skip it instead." (cond ((and (not (eobp)) (python-info-current-line-empty-p)) (python-util-forward-comment dir) - (python-nav--forward-sexp dir)) + (python-nav--forward-sexp dir safe skip-parens-p)) ((eq context 'block-start) (python-nav-end-of-block)) ((eq context 'statement-start) @@ -1656,7 +1660,7 @@ errors when at end of sexp, skip it instead." (cond ((and (not (bobp)) (python-info-current-line-empty-p)) (python-util-forward-comment dir) - (python-nav--forward-sexp dir)) + (python-nav--forward-sexp dir safe skip-parens-p)) ((eq context 'block-end) (python-nav-beginning-of-block)) ((eq context 'statement-end) @@ -1674,47 +1678,69 @@ errors when at end of sexp, skip it instead." (python-nav-beginning-of-statement)) (t (goto-char next-sexp-pos)))))))))) -(defun python-nav-forward-sexp (&optional arg) +(defun python-nav-forward-sexp (&optional arg safe skip-parens-p) "Move forward across expressions. With ARG, do it that many times. Negative arg -N means move -backward N times." +backward N times. When optional argument SAFE is non-nil do not +throw errors when at end of sexp, skip it instead. With optional +argument SKIP-PARENS-P force sexp motion to ignore parenthesized +expressions when looking at them in either direction (forced to t +in interactive calls)." (interactive "^p") (or arg (setq arg 1)) + ;; Do not follow parens on interactive calls. This hack to detect + ;; if the function was called interactively copes with the way + ;; `forward-sexp' works by calling `forward-sexp-function', losing + ;; interactive detection by checking `current-prefix-arg'. The + ;; reason to make this distinction is that lisp functions like + ;; `blink-matching-open' get confused causing issues like the one in + ;; Bug#16191. With this approach the user gets a symmetric behavior + ;; when working interactively while called functions expecting + ;; paren-based sexp motion work just fine. + (or + skip-parens-p + (setq skip-parens-p + (memq real-this-command + (list + #'forward-sexp #'backward-sexp + #'python-nav-forward-sexp #'python-nav-backward-sexp + #'python-nav-forward-sexp-safe #'python-nav-backward-sexp)))) (while (> arg 0) - (python-nav--forward-sexp 1) + (python-nav--forward-sexp 1 safe skip-parens-p) (setq arg (1- arg))) (while (< arg 0) - (python-nav--forward-sexp -1) + (python-nav--forward-sexp -1 safe skip-parens-p) (setq arg (1+ arg)))) -(defun python-nav-backward-sexp (&optional arg) +(defun python-nav-backward-sexp (&optional arg safe skip-parens-p) "Move backward across expressions. With ARG, do it that many times. Negative arg -N means move -forward N times." +forward N times. When optional argument SAFE is non-nil do not +throw errors when at end of sexp, skip it instead. With optional +argument SKIP-PARENS-P force sexp motion to ignore parenthesized +expressions when looking at them in either direction (forced to t +in interactive calls)." (interactive "^p") (or arg (setq arg 1)) - (python-nav-forward-sexp (- arg))) + (python-nav-forward-sexp (- arg) safe skip-parens-p)) -(defun python-nav-forward-sexp-safe (&optional arg) +(defun python-nav-forward-sexp-safe (&optional arg skip-parens-p) "Move forward safely across expressions. With ARG, do it that many times. Negative arg -N means move -backward N times." +backward N times. With optional argument SKIP-PARENS-P force +sexp motion to ignore parenthesized expressions when looking at +them in either direction (forced to t in interactive calls)." (interactive "^p") - (or arg (setq arg 1)) - (while (> arg 0) - (python-nav--forward-sexp 1 t) - (setq arg (1- arg))) - (while (< arg 0) - (python-nav--forward-sexp -1 t) - (setq arg (1+ arg)))) + (python-nav-forward-sexp arg t skip-parens-p)) -(defun python-nav-backward-sexp-safe (&optional arg) +(defun python-nav-backward-sexp-safe (&optional arg skip-parens-p) "Move backward safely across expressions. With ARG, do it that many times. Negative arg -N means move -forward N times." +forward N times. With optional argument SKIP-PARENS-P force sexp +motion to ignore parenthesized expressions when looking at them in +either direction (forced to t in interactive calls)." (interactive "^p") - (or arg (setq arg 1)) - (python-nav-forward-sexp-safe (- arg))) + (python-nav-backward-sexp arg t skip-parens-p)) (defun python-nav--up-list (&optional dir) "Internal implementation of `python-nav-up-list'. @@ -2971,25 +2997,25 @@ This function takes the list of setup code to send from the (defcustom python-shell-completion-setup-code "try: - import __builtin__ -except ImportError: - # Python 3 - import builtins as __builtin__ -try: - import readline, rlcompleter + import readline except: def __PYTHON_EL_get_completions(text): return [] else: def __PYTHON_EL_get_completions(text): + try: + import __builtin__ + except ImportError: + # Python 3 + import builtins as __builtin__ builtins = dir(__builtin__) completions = [] + is_ipython = ('__IPYTHON__' in builtins or + '__IPYTHON__active' in builtins) + splits = text.split() + is_module = splits and splits[0] in ('from', 'import') try: - splits = text.split() - is_module = splits and splits[0] in ('from', 'import') - is_ipython = ('__IPYTHON__' in builtins or - '__IPYTHON__active' in builtins) - if is_module: + if is_ipython and is_module: from IPython.core.completerlib import module_completion completions = module_completion(text.strip()) elif is_ipython and '__IP' in builtins: @@ -2997,13 +3023,20 @@ else: elif is_ipython and 'get_ipython' in builtins: completions = get_ipython().Completer.all_completions(text) else: + # Try to reuse current completer. + completer = readline.get_completer() + if not completer: + # importing rlcompleter sets the completer, use it as a + # last resort to avoid breaking customizations. + import rlcompleter + completer = readline.get_completer() i = 0 while True: - res = readline.get_completer()(text, i) - if not res: + completion = completer(text, i) + if not completion: break i += 1 - completions.append(res) + completions.append(completion) except: pass return completions" @@ -3042,10 +3075,14 @@ When a match is found, native completion is disabled." "Enable readline based native completion." :type 'boolean) -(defcustom python-shell-completion-native-output-timeout 0.01 +(defcustom python-shell-completion-native-output-timeout 5.0 "Time in seconds to wait for completion output before giving up." :type 'float) +(defcustom python-shell-completion-native-try-output-timeout 1.0 + "Time in seconds to wait for *trying* native completion output." + :type 'float) + (defvar python-shell-completion-native-redirect-buffer " *Python completions redirect*" "Buffer to be used to redirect output of readline commands.") @@ -3059,7 +3096,9 @@ When a match is found, native completion is disabled." (defun python-shell-completion-native-try () "Return non-nil if can trigger native completion." - (let ((python-shell-completion-native-enable t)) + (let ((python-shell-completion-native-enable t) + (python-shell-completion-native-output-timeout + python-shell-completion-native-try-output-timeout)) (python-shell-completion-native-get-completions (get-buffer-process (current-buffer)) nil "int"))) @@ -3067,27 +3106,73 @@ When a match is found, native completion is disabled." (defun python-shell-completion-native-setup () "Try to setup native completion, return non-nil on success." (let ((process (python-shell-get-process))) - (python-shell-send-string - (funcall - 'mapconcat - #'identity - (list - "try:" - " import readline, rlcompleter" - ;; Remove parens on callables as it breaks completion on - ;; arguments (e.g. str(Ari<tab>)). - " class Completer(rlcompleter.Completer):" - " def _callable_postfix(self, val, word):" - " return word" - " readline.set_completer(Completer().complete)" - " if readline.__doc__ and 'libedit' in readline.__doc__:" - " readline.parse_and_bind('bind ^I rl_complete')" - " else:" - " readline.parse_and_bind('tab: complete')" - " print ('python.el: readline is available')" - "except:" - " print ('python.el: readline not available')") - "\n") + (python-shell-send-string " +def __PYTHON_EL_native_completion_setup(): + try: + import readline + try: + import __builtin__ + except ImportError: + # Python 3 + import builtins as __builtin__ + builtins = dir(__builtin__) + is_ipython = ('__IPYTHON__' in builtins or + '__IPYTHON__active' in builtins) + class __PYTHON_EL_Completer: + PYTHON_EL_WRAPPED = True + def __init__(self, completer): + self.completer = completer + self.last_completion = None + def __call__(self, text, state): + if state == 0: + # The first completion is always a dummy completion. This + # ensures proper output for sole completions and a current + # input safeguard when no completions are available. + self.last_completion = None + completion = '0__dummy_completion__' + else: + completion = self.completer(text, state - 1) + if not completion: + if state == 1: + # When no completions are available, two non-sharing + # prefix strings are returned just to ensure output + # while preventing changes to current input. + completion = '1__dummy_completion__' + elif self.last_completion != '~~~~__dummy_completion__': + # This marks the end of output. + completion = '~~~~__dummy_completion__' + elif completion.endswith('('): + # Remove parens on callables as it breaks completion on + # arguments (e.g. str(Ari<tab>)). + completion = completion[:-1] + self.last_completion = completion + return completion + completer = readline.get_completer() + if not completer: + # Used as last resort to avoid breaking customizations. + import rlcompleter + completer = readline.get_completer() + if completer and not getattr(completer, 'PYTHON_EL_WRAPPED', False): + # Wrap the existing completer function only once. + new_completer = __PYTHON_EL_Completer(completer) + if not is_ipython: + readline.set_completer(new_completer) + else: + # IPython hacks readline such that `readline.set_completer` + # won't work. This workaround injects the new completer + # function into the existing instance directly. + instance = getattr(completer, 'im_self', completer.__self__) + instance.rlcomplete = new_completer + if readline.__doc__ and 'libedit' in readline.__doc__: + readline.parse_and_bind('bind ^I rl_complete') + else: + readline.parse_and_bind('tab: complete') + # Require just one tab to send output. + readline.parse_and_bind('set show-all-if-ambiguous on') + print ('python.el: readline is available') + except IOError: + print ('python.el: readline not available') +__PYTHON_EL_native_completion_setup()" process) (python-shell-accept-process-output process) (when (save-excursion @@ -3165,9 +3250,8 @@ completion." (original-filter-fn (process-filter process)) (redirect-buffer (get-buffer-create python-shell-completion-native-redirect-buffer)) - (separators (python-rx - (or whitespace open-paren close-paren))) - (trigger "\t\t\t") + (separators (python-rx (or whitespace open-paren close-paren))) + (trigger "\t") (new-input (concat input trigger)) (input-length (save-excursion @@ -3189,7 +3273,8 @@ completion." (comint-redirect-insert-matching-regexp nil) ;; Feed it some regex that will never match. (comint-redirect-finished-regexp "^\\'$") - (comint-redirect-output-buffer redirect-buffer)) + (comint-redirect-output-buffer redirect-buffer) + (current-time (float-time))) ;; Compatibility with Emacs 24.x. Comint changed and ;; now `comint-redirect-filter' gets 3 args. This ;; checks which version of `comint-redirect-filter' is @@ -3202,22 +3287,22 @@ completion." #'comint-redirect-filter original-filter-fn)) (set-process-filter process #'comint-redirect-filter)) (process-send-string process input-to-send) - (accept-process-output - process - python-shell-completion-native-output-timeout) - ;; XXX: can't use `python-shell-accept-process-output' - ;; here because there are no guarantees on how output - ;; ends. The workaround here is to call - ;; `accept-process-output' until we don't find anything - ;; else to accept. - (while (accept-process-output - process - python-shell-completion-native-output-timeout)) + ;; Grab output until our dummy completion used as + ;; output end marker is found. Output is accepted + ;; *very* quickly to keep the shell super-responsive. + (while (and (not (re-search-backward "~~~~__dummy_completion__" nil t)) + (< (- current-time (float-time)) + python-shell-completion-native-output-timeout)) + (accept-process-output process 0.01)) (cl-remove-duplicates - (split-string - (buffer-substring-no-properties - (point-min) (point-max)) - separators t)))) + (cl-remove-if + (lambda (c) + (string-match "__dummy_completion__" c)) + (split-string + (buffer-substring-no-properties + (point-min) (point-max)) + separators t)) + :test #'string=))) (set-process-filter process original-filter-fn)))))) (defun python-shell-completion-get-completions (process import input) @@ -3587,17 +3672,12 @@ JUSTIFY should be used (if applicable) as in `fill-paragraph'." (`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 str-start-pos str-end-pos) (fill-paragraph justify)) (save-excursion - (when (and docstring-p python-fill-docstring-style) + (when (and (python-info-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)) @@ -4423,23 +4503,40 @@ where the continued line ends." (when (looking-at (python-rx block-start)) (point-marker))))) +(defun python-info-assignment-statement-p (&optional current-line-only) + "Check if current line is an assignment. +With argument CURRENT-LINE-ONLY is non-nil, don't follow any +continuations, just check the if current line is an assignment." + (save-excursion + (let ((found nil)) + (if current-line-only + (back-to-indentation) + (python-nav-beginning-of-statement)) + (while (and + (re-search-forward (python-rx not-simple-operator + assignment-operator + (group not-simple-operator)) + (line-end-position) t) + (not found)) + (save-excursion + ;; The assignment operator should not be inside a string. + (backward-char (length (match-string-no-properties 1))) + (setq found (not (python-syntax-context-type))))) + (when found + (skip-syntax-forward " ") + (point-marker))))) + +;; TODO: rename to clarify this is only for the first continuation +;; line or remove it and move its body to `python-indent-context'. (defun python-info-assignment-continuation-line-p () - "Check if current line is a continuation of an assignment. + "Check if current line is the first continuation of an assignment. When current line is continuation of another with an assignment return the point of the first non-blank character after the operator." (save-excursion (when (python-info-continuation-line-p) (forward-line -1) - (back-to-indentation) - (when (and (not (looking-at (python-rx block-start))) - (and (re-search-forward (python-rx not-simple-operator - assignment-operator - not-simple-operator) - (line-end-position) t) - (not (python-syntax-context-type)))) - (skip-syntax-forward "\s") - (point-marker))))) + (python-info-assignment-statement-p t)))) (defun python-info-looking-at-beginning-of-defun (&optional syntax-ppss) "Check if point is at `beginning-of-defun' using SYNTAX-PPSS." @@ -4464,6 +4561,46 @@ operator." (* whitespace) line-end)) (string-equal "" (match-string-no-properties 1)))) +(defun python-info-docstring-p (&optional syntax-ppss) + "Return non-nil if point is in a docstring. +When optional argument SYNTAX-PPSS is given, use that instead of +point's current `syntax-ppss'." + ;;; https://www.python.org/dev/peps/pep-0257/#what-is-a-docstring + (save-excursion + (when (and syntax-ppss (python-syntax-context 'string syntax-ppss)) + (goto-char (nth 8 syntax-ppss))) + (python-nav-beginning-of-statement) + (let ((counter 1) + (indentation (current-indentation)) + (backward-sexp-point) + (re (concat "[uU]?[rR]?" + (python-rx string-delimiter)))) + (when (and + (not (python-info-assignment-statement-p)) + (looking-at-p re) + ;; Allow up to two consecutive docstrings only. + (>= + 2 + (progn + (while (save-excursion + (python-nav-backward-sexp) + (setq backward-sexp-point (point)) + (and (= indentation (current-indentation)) + (not (bobp)) ; Prevent infloop. + (looking-at-p + (concat "[uU]?[rR]?" + (python-rx string-delimiter))))) + ;; Previous sexp was a string, restore point. + (goto-char backward-sexp-point) + (cl-incf counter)) + counter))) + (python-util-forward-comment -1) + (python-nav-beginning-of-statement) + (cond ((bobp)) + ((python-info-assignment-statement-p) t) + ((python-info-looking-at-beginning-of-defun)) + (t nil)))))) + (defun python-info-encoding-from-cookie () "Detect current buffer's encoding from its coding cookie. Returns the encoding as a symbol." |