diff options
Diffstat (limited to 'lisp/progmodes/python.el')
-rw-r--r-- | lisp/progmodes/python.el | 702 |
1 files changed, 360 insertions, 342 deletions
diff --git a/lisp/progmodes/python.el b/lisp/progmodes/python.el index 13ff439bef2..d340550a017 100644 --- a/lisp/progmodes/python.el +++ b/lisp/progmodes/python.el @@ -463,9 +463,14 @@ The type returned can be `comment', `string' or `paren'." ((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))) +(defsubst python-syntax-comment-or-string-p (&optional ppss) + "Return non-nil if PPSS is inside 'comment or 'string." + (nth 8 (or ppss (syntax-ppss)))) + +(defsubst python-syntax-closing-paren-p () + "Return non-nil if char after point is a closing paren." + (= (syntax-class (syntax-after (point))) + (syntax-class (string-to-syntax ")")))) (define-obsolete-function-alias 'python-info-ppss-context #'python-syntax-context "24.3") @@ -704,11 +709,28 @@ It makes underscores and dots word constituent chars.") '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.") + "Deprecated var available for compatibility.") (defvar python-indent-levels '(0) - "Levels of indentation available for `python-indent-line-function'. -Can also be `noindent' if automatic indentation can't be used.") + "Deprecated var available for compatibility.") + +(make-obsolete-variable + 'python-indent-current-level + "The indentation API changed to avoid global state. +The function `python-indent-calculate-levels' does not use it +anymore. If you were defadvising it and or depended on this +variable for indentation customizations, refactor your code to +work on `python-indent-calculate-indentation' instead." + "24.5") + +(make-obsolete-variable + 'python-indent-levels + "The indentation API changed to avoid global state. +The function `python-indent-calculate-levels' does not use it +anymore. If you were defadvising it and or depended on this +variable for indentation customizations, refactor your code to +work on `python-indent-calculate-indentation' instead." + "24.5") (defun python-indent-guess-indent-offset () "Guess and set `python-indent-offset' for the current buffer." @@ -748,362 +770,358 @@ Can also be `noindent' if automatic indentation can't be used.") python-indent-offset))))))) (defun python-indent-context () - "Get information on indentation context. -Context information is returned with a cons with the form: - (STATUS . START) - -Where status can be any of the following symbols: - - * after-comment: When current line might continue a comment block - * inside-paren: If point in between (), {} or [] - * inside-string: If point is inside a string - * after-backslash: Previous line ends in a backslash - * after-beginning-of-block: Point is after beginning of block - * after-line: Point is after normal line - * dedenter-statement: Point is on a dedenter statement. - * no-indent: Point is at beginning of buffer or other special case -START is the buffer position where the sexp starts." + "Get information about the current indentation context. +Context is returned in a cons with the form (STATUS . START). + +STATUS can be one of the following: + +keyword +------- + +:after-comment + - Point is after a comment line. + - START is the position of the \"#\" character. +:inside-string + - Point is inside string. + - START is the position of the first quote that starts it. +:no-indent + - No possible indentation case matches. + - START is always zero. + +:inside-paren + - Fallback case when point is inside paren. + - START is the first non space char position *after* the open paren. +:inside-paren-at-closing-nested-paren + - Point is on a line that contains a nested paren closer. + - START is the position of the open paren it closes. +:inside-paren-at-closing-paren + - Point is on a line that contains a paren closer. + - START is the position of the open paren. +:inside-paren-newline-start + - Point is inside a paren with items starting in their own line. + - START is the position of the open paren. +:inside-paren-newline-start-from-block + - Point is inside a paren with items starting in their own line + from a block start. + - START is the position of the open paren. + +:after-backslash + - Fallback case when point is after backslash. + - START is the char after the position of the backslash. +:after-backslash-assignment-continuation + - Point is after a backslashed assignment. + - START is the char after the position of the backslash. +:after-backslash-block-continuation + - Point is after a backslashed block continuation. + - START is the char after the position of the backslash. +:after-backslash-dotted-continuation + - Point is after a backslashed dotted continuation. Previous + line must contain a dot to align with. + - START is the char after the position of the backslash. +:after-backslash-first-line + - First line following a backslashed continuation. + - START is the char after the position of the backslash. + +:after-block-end + - Point is after a line containing a block ender. + - START is the position where the ender starts. +:after-block-start + - Point is after a line starting a block. + - START is the position where the block starts. +:after-line + - Point is after a simple line. + - START is the position where the previous line starts. +:at-dedenter-block-start + - Point is on a line starting a dedenter block. + - START is the position where the dedenter block starts." (save-restriction (widen) - (let ((ppss (save-excursion (beginning-of-line) (syntax-ppss))) - (start)) - (cons - (cond - ;; Beginning of buffer - ((save-excursion - (goto-char (line-beginning-position)) - (bobp)) - 'no-indent) - ;; Comment continuation - ((save-excursion - (when (and - (or - (python-info-current-line-comment-p) - (python-info-current-line-empty-p)) - (progn - (forward-comment -1) - (python-info-current-line-comment-p))) - (setq start (point)) - 'after-comment))) - ;; Inside string - ((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-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 - (when (progn - (back-to-indentation) - (python-util-forward-comment -1) - (equal (char-before) ?:)) - ;; Move to the first block start that's not in within - ;; a string, comment or paren and that's not a - ;; continuation line. - (while (and (re-search-backward - (python-rx block-start) nil t) - (or - (python-syntax-context-type) - (python-info-continuation-line-p)))) - (when (looking-at (python-rx block-start)) - (point-marker))))) - 'after-beginning-of-block) - ((when (setq start (python-info-dedenter-statement-p)) - 'dedenter-statement)) - ;; After normal line - ((setq start (save-excursion + (let ((ppss (save-excursion + (beginning-of-line) + (syntax-ppss)))) + (cond + ;; Beginning of buffer. + ((= (line-number-at-pos) 1) + (cons :no-indent 0)) + ;; Comment continuation (maybe). + ((save-excursion + (when (and + (or + (python-info-current-line-comment-p) + (python-info-current-line-empty-p)) + (forward-comment -1) + (python-info-current-line-comment-p)) + (cons :after-comment (point))))) + ;; Inside a string. + ((let ((start (python-syntax-context 'string ppss))) + (when start + (cons :inside-string start)))) + ;; Inside a paren. + ((let* ((start (python-syntax-context 'paren ppss)) + (starts-in-newline + (when start + (save-excursion + (goto-char start) + (forward-char) + (not + (= (line-number-at-pos) + (progn + (python-util-forward-comment) + (line-number-at-pos)))))))) + (when start + (cond + ;; Current line only holds the closing paren. + ((save-excursion + (skip-syntax-forward " ") + (when (and (python-syntax-closing-paren-p) + (progn + (forward-char 1) + (not (python-syntax-context 'paren)))) + (cons :inside-paren-at-closing-paren start)))) + ;; Current line only holds a closing paren for nested. + ((save-excursion + (back-to-indentation) + (python-syntax-closing-paren-p)) + (cons :inside-paren-at-closing-nested-paren start)) + ;; This line starts from a opening block in its own line. + ((save-excursion + (goto-char start) + (when (and + starts-in-newline + (save-excursion + (back-to-indentation) + (looking-at (python-rx block-start)))) + (cons + :inside-paren-newline-start-from-block start)))) + (starts-in-newline + (cons :inside-paren-newline-start start)) + ;; General case. + (t (cons :inside-paren + (save-excursion + (goto-char (1+ start)) + (skip-syntax-forward "(" 1) + (skip-syntax-forward " ") + (point)))))))) + ;; After backslash. + ((let ((start (when (not (python-syntax-comment-or-string-p ppss)) + (python-info-line-ends-backslash-p + (1- (line-number-at-pos)))))) + (when start + (cond + ;; Continuation of dotted expression. + ((save-excursion + (back-to-indentation) + (when (eq (char-after) ?\.) + ;; Move point back until it's not inside a paren. + (while (prog2 + (forward-line -1) + (and (not (bobp)) + (python-syntax-context 'paren)))) + (goto-char (line-end-position)) + (while (and (search-backward + "." (line-beginning-position) t) + (python-syntax-context-type))) + ;; Ensure previous statement has dot to align with. + (when (and (eq (char-after) ?\.) + (not (python-syntax-context-type))) + (cons :after-backslash-dotted-continuation (point)))))) + ;; Continuation of block definition. + ((let ((block-continuation-start + (python-info-block-continuation-line-p))) + (when block-continuation-start + (save-excursion + (goto-char block-continuation-start) + (re-search-forward + (python-rx block-start (* space)) + (line-end-position) t) + (cons :after-backslash-block-continuation (point)))))) + ;; Continuation of assignment. + ((let ((assignment-continuation-start + (python-info-assignment-continuation-line-p))) + (when assignment-continuation-start + (save-excursion + (goto-char assignment-continuation-start) + (cons :after-backslash-assignment-continuation (point)))))) + ;; First line after backslash continuation start. + ((save-excursion + (goto-char start) + (when (or (= (line-number-at-pos) 1) + (not (python-info-beginning-of-backslash + (1- (line-number-at-pos))))) + (cons :after-backslash-first-line start)))) + ;; General case. + (t (cons :after-backslash start)))))) + ;; After beginning of block. + ((let ((start (save-excursion + (back-to-indentation) + (python-util-forward-comment -1) + (when (equal (char-before) ?:) + (python-nav-beginning-of-block))))) + (when start + (cons :after-block-start start)))) + ;; At dedenter statement. + ((let ((start (python-info-dedenter-statement-p))) + (when start + (cons :at-dedenter-block-start start)))) + ;; After normal line. + ((let ((start (save-excursion (back-to-indentation) - (skip-chars-backward (rx (or whitespace ?\n))) + (skip-chars-backward " \t\n") (python-nav-beginning-of-statement) - (point-marker))) - 'after-line) - ;; Do not indent - (t 'no-indent)) - start)))) - -(defun python-indent-calculate-indentation () - "Calculate correct indentation offset for the current line. -Returns `noindent' if the indentation does not depend on Python syntax, -such as in strings." - (let* ((indentation-context (python-indent-context)) - (context-status (car indentation-context)) - (context-start (cdr indentation-context))) - (save-restriction - (widen) - (save-excursion - (pcase context-status - (`no-indent 0) - (`after-comment - (goto-char context-start) - (current-indentation)) - ;; When point is after beginning of block just add one level - ;; of indentation relative to the context-start - (`after-beginning-of-block - (goto-char context-start) - (+ (current-indentation) python-indent-offset)) - ;; When after a simple line just use previous line - ;; indentation. - (`after-line - (let* ((pair (save-excursion - (goto-char context-start) - (cons - (current-indentation) - (python-info-beginning-of-block-p)))) - (context-indentation (car pair)) - ;; TODO: Separate block enders into its own case. - (adjustment - (if (save-excursion - (python-util-forward-comment -1) - (python-nav-beginning-of-statement) - (looking-at (python-rx block-ender))) - python-indent-offset - 0))) - (- context-indentation adjustment))) - ;; When point is on a dedenter statement, search for the - ;; opening block that corresponds to it and use its - ;; indentation. If no opening block is found just remove - ;; indentation as this is an invalid python file. - (`dedenter-statement - (let ((block-start-point - (python-info-dedenter-opening-block-position))) - (save-excursion - (if (not block-start-point) - 0 - (goto-char block-start-point) - (current-indentation))))) - ;; 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 'noindent) - ;; 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 - ;; line must contain a dot too. - ((save-excursion - (back-to-indentation) - (when (looking-at "\\.") - ;; If after moving one line back point is inside a paren it - ;; needs to move back until it's not anymore - (while (prog2 - (forward-line -1) - (and (not (bobp)) - (python-syntax-context 'paren)))) - (goto-char (line-end-position)) - (while (and (re-search-backward - "\\." (line-beginning-position) t) - (python-syntax-context-type))) - (if (and (looking-at "\\.") - (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 - (current-column) - ;; No dot found on previous line, just add another - ;; indentation level. - (+ (current-indentation) python-indent-offset))))) - ;; Check if prev line is a block continuation - ((let ((block-continuation-start - (python-info-block-continuation-line-p))) - (when block-continuation-start - ;; If block-continuation-start is set jump to that - ;; marker and use first column after the block start - ;; as indentation value. - (goto-char block-continuation-start) - (re-search-forward - (python-rx block-start (* space)) - (line-end-position) t) - (current-column)))) - ;; Check if current line is an assignment continuation - ((let ((assignment-continuation-start - (python-info-assignment-continuation-line-p))) - (when assignment-continuation-start - ;; If assignment-continuation is set jump to that - ;; marker and use first column after the assignment - ;; operator as indentation value. - (goto-char assignment-continuation-start) - (current-column)))) - (t - (forward-line -1) - (goto-char (python-info-beginning-of-backslash)) - (if (save-excursion - (and - (forward-line -1) - (goto-char - (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. - (current-indentation) - ;; What happens here is that we are dealing with the second - ;; line of a backslash continuation, in that case we just going - ;; to add one indentation level. - (+ (current-indentation) python-indent-offset))))) - ;; When inside a paren there's a need to handle nesting - ;; correctly - (`inside-paren - (cond - ;; 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-syntax-context 'paren)))) - (goto-char context-start) - (current-indentation)))) - ;; If open paren is contained on a line by itself add another - ;; indentation level, else look for the first word after the - ;; opening paren and use it's column position as indentation - ;; level. - ((let* ((content-starts-in-newline) - (indent - (save-excursion - (if (setq content-starts-in-newline - (progn - (goto-char context-start) - (forward-char) - (save-restriction - (narrow-to-region - (line-beginning-position) - (line-end-position)) - (python-util-forward-comment)) - (looking-at "$"))) - (+ (current-indentation) python-indent-offset) - (current-column))))) - ;; Adjustments - (cond - ;; If current line closes a nested open paren de-indent one - ;; level. - ((progn - (back-to-indentation) - (looking-at (regexp-opt '(")" "]" "}")))) - (- indent python-indent-offset)) - ;; If the line of the opening paren that wraps the current - ;; line starts a block add another level of indentation to - ;; follow new pep8 recommendation. See: http://ur1.ca/5rojx - ((save-excursion - (when (and content-starts-in-newline - (progn - (goto-char context-start) - (back-to-indentation) - (looking-at (python-rx block-start)))) - (+ indent python-indent-offset)))) - (t indent))))))))))) - -(defun python-indent-calculate-levels () - "Calculate `python-indent-levels' and reset `python-indent-current-level'." - (if (or (python-info-continuation-line-p) - (not (python-info-dedenter-statement-p))) - ;; XXX: This asks for a refactor. Even if point is on a - ;; dedenter statement, it could be multiline and in that case - ;; the continuation lines should be indented with normal rules. - (let* ((indentation (python-indent-calculate-indentation))) - (if (not (numberp indentation)) - (setq python-indent-levels indentation) - (let* ((remainder (% indentation python-indent-offset)) - (steps (/ (- indentation remainder) python-indent-offset))) - (setq python-indent-levels (list 0)) - (dotimes (step steps) - (push (* python-indent-offset (1+ step)) python-indent-levels)) - (when (not (eq 0 remainder)) - (push (+ (* python-indent-offset steps) remainder) - python-indent-levels))))) - (setq python-indent-levels - (or - (mapcar (lambda (pos) - (save-excursion - (goto-char pos) - (current-indentation))) - (python-info-dedenter-opening-block-positions)) - (list 0)))) - (when (listp python-indent-levels) - (setq python-indent-current-level (1- (length python-indent-levels)) - python-indent-levels (nreverse python-indent-levels)))) - -(defun python-indent-toggle-levels () - "Toggle `python-indent-current-level' over `python-indent-levels'." - (setq python-indent-current-level (1- python-indent-current-level)) - (when (< python-indent-current-level 0) - (setq python-indent-current-level (1- (length python-indent-levels))))) - -(defun python-indent-line (&optional force-toggle) + (point)))) + (when start + (if (save-excursion + (python-util-forward-comment -1) + (python-nav-beginning-of-statement) + (looking-at (python-rx block-ender))) + (cons :after-block-end start) + (cons :after-line start))))) + ;; Default case: do not indent. + (t (cons :no-indent 0)))))) + +(defun python-indent--calculate-indentation () + "Internal implementation of `python-indent-calculate-indentation'. +May return an integer for the maximum possible indentation at +current context or a list of integers. The latter case is only +happening for :at-dedenter-block-start context since the +possibilities can be narrowed to especific indentation points." + (save-restriction + (widen) + (save-excursion + (pcase (python-indent-context) + (`(:no-indent . ,_) 0) + (`(,(or :after-line + :after-comment + :inside-string + :after-backslash + :inside-paren-at-closing-paren + :inside-paren-at-closing-nested-paren) . ,start) + ;; Copy previous indentation. + (goto-char start) + (current-indentation)) + (`(,(or :after-block-start + :after-backslash-first-line + :inside-paren-newline-start) . ,start) + ;; Add one indentation level. + (goto-char start) + (+ (current-indentation) python-indent-offset)) + (`(,(or :inside-paren + :after-backslash-block-continuation + :after-backslash-assignment-continuation + :after-backslash-dotted-continuation) . ,start) + ;; Use the column given by the context. + (goto-char start) + (current-column)) + (`(:after-block-end . ,start) + ;; Subtract one indentation level. + (goto-char start) + (- (current-indentation) python-indent-offset)) + (`(:at-dedenter-block-start . ,_) + ;; List all possible indentation levels from opening blocks. + (let ((opening-block-start-points + (python-info-dedenter-opening-block-positions))) + (if (not opening-block-start-points) + 0 ; if not found default to first column + (mapcar (lambda (pos) + (save-excursion + (goto-char pos) + (current-indentation))) + opening-block-start-points)))) + (`(,(or :inside-paren-newline-start-from-block) . ,start) + ;; Add two indentation levels to make the suite stand out. + (goto-char start) + (+ (current-indentation) (* python-indent-offset 2))))))) + +(defun python-indent--calculate-levels (indentation) + "Calculate levels list given INDENTATION. +Argument INDENTATION can either be an integer or a list of +integers. Levels are returned in ascending order, and in the +case INDENTATION is a list, this order is enforced." + (if (listp indentation) + (sort (copy-sequence indentation) #'<) + (let* ((remainder (% indentation python-indent-offset)) + (steps (/ (- indentation remainder) python-indent-offset)) + (levels (mapcar (lambda (step) + (* python-indent-offset step)) + (number-sequence steps 0 -1)))) + (reverse + (if (not (zerop remainder)) + (cons indentation levels) + levels))))) + +(defun python-indent--previous-level (levels indentation) + "Return previous level from LEVELS relative to INDENTATION." + (let* ((levels (sort (copy-sequence levels) #'>)) + (default (car levels))) + (catch 'return + (dolist (level levels) + (when (funcall #'< level indentation) + (throw 'return level))) + default))) + +(defun python-indent-calculate-indentation (&optional previous) + "Calculate indentation. +Get indentation of PREVIOUS level when argument is non-nil. +Return the max level of the cycle when indentation reaches the +minimum." + (let* ((indentation (python-indent--calculate-indentation)) + (levels (python-indent--calculate-levels indentation))) + (if previous + (python-indent--previous-level levels (current-indentation)) + (apply #'max levels)))) + +(defun python-indent-line (&optional previous) "Internal implementation of `python-indent-line-function'. -Uses the offset calculated in -`python-indent-calculate-indentation' and available levels -indicated by the variable `python-indent-levels' to set the -current indentation. +Use the PREVIOUS level when argument is non-nil, otherwise indent +to the maxium available level. When indentation is the minimum +possible and PREVIOUS is non-nil, cycle back to the maximum +level." + (let ((follow-indentation-p + ;; Check if point is within indentation. + (and (<= (line-beginning-position) (point)) + (>= (+ (line-beginning-position) + (current-indentation)) + (point))))) + (save-excursion + (indent-line-to + (python-indent-calculate-indentation previous)) + (python-info-dedenter-opening-block-message)) + (when follow-indentation-p + (back-to-indentation)))) -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 them -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 (and (or (and (memq this-command python-indent-trigger-commands) - (eq last-command this-command)) - force-toggle) - (not (equal python-indent-levels '(0)))) - (if (listp python-indent-levels) - (python-indent-toggle-levels)) - (python-indent-calculate-levels)) - (if (eq python-indent-levels 'noindent) - python-indent-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-dedenter-opening-block-message))) +(defun python-indent-calculate-levels () + "Return possible indentation levels." + (python-indent--calculate-levels + (python-indent--calculate-indentation))) (defun python-indent-line-function () "`indent-line-function' for Python mode. -See `python-indent-line' for details." - (python-indent-line)) +When the variable `last-command' is equal to one of the symbols +inside `python-indent-trigger-commands' it cycles possible +indentation levels from right to left." + (python-indent-line + (and (memq this-command python-indent-trigger-commands) + (eq last-command this-command)))) (defun python-indent-dedent-line () "De-indent current line." (interactive "*") - (when (and (not (python-syntax-comment-or-string-p)) - (<= (point) (save-excursion - (back-to-indentation) - (point))) - (> (current-column) 0)) - (python-indent-line t) - t)) + (when (and (not (bolp)) + (not (python-syntax-comment-or-string-p)) + (= (+ (line-beginning-position) + (current-indentation)) + (point))) + (python-indent-line t) + t)) (defun python-indent-dedent-line-backspace (arg) "De-indent current line. Argument ARG is passed to `backward-delete-char-untabify' when point is not in between the indentation." (interactive "*p") - (when (not (python-indent-dedent-line)) + (unless (python-indent-dedent-line) (backward-delete-char-untabify arg))) + (put 'python-indent-dedent-line-backspace 'delete-selection 'supersede) (defun python-indent-region (start end) |