]> git.eshelyaron.com Git - emacs.git/commitdiff
python.el: New non-global state dependent indentation engine.
authorFabián Ezequiel Gallina <fgallina@gnu.org>
Tue, 27 Jan 2015 03:17:24 +0000 (00:17 -0300)
committerFabián Ezequiel Gallina <fgallina@gnu.org>
Tue, 27 Jan 2015 03:17:24 +0000 (00:17 -0300)
Fixes: debbugs:18319
Fixes: debbugs:19595
* lisp/progmodes/python.el (python-syntax-comment-or-string-p): Accept
PPSS as argument.
(python-syntax-closing-paren-p): New function.
(python-indent-current-level)
(python-indent-levels): Mark obsolete.
(python-indent-context): Return more context cases.
(python-indent--calculate-indentation)
(python-indent--calculate-levels): New functions.
(python-indent-calculate-levels): Use them.
(python-indent-calculate-indentation, python-indent-line):
(python-indent-line-function): Rewritten to use new API.
(python-indent-dedent-line): Simplify logic.
(python-indent-dedent-line-backspace): Use `unless`.
(python-indent-toggle-levels): Delete function.

* test/automated/python-tests.el (python-indent-pep8-1)
(python-indent-pep8-2, python-indent-pep8-3)
(python-indent-after-comment-1, python-indent-after-comment-2)
(python-indent-inside-paren-1, python-indent-inside-paren-2)
(python-indent-after-block-1, python-indent-after-block-2)
(python-indent-after-backslash-1, python-indent-after-backslash-2)
(python-indent-after-backslash-3, python-indent-block-enders-1)
(python-indent-block-enders-2, python-indent-block-enders-3)
(python-indent-block-enders-4, python-indent-block-enders-5)
(python-indent-dedenters-1, python-indent-dedenters-2)
(python-indent-dedenters-3, python-indent-dedenters-4)
(python-indent-dedenters-5, python-indent-dedenters-6)
(python-indent-dedenters-7, python-indent-dedenters-8): Fix tests.
(python-indent-base-case, python-indent-after-block-3)
(python-indent-after-backslash-5, python-indent-inside-paren-3)
(python-indent-inside-paren-4, python-indent-inside-paren-5)
(python-indent-inside-paren-6, python-indent-inside-string-1)
(python-indent-inside-string-2, python-indent-inside-string-3)
(python-indent-dedent-line-backspace-1): New Tests.

lisp/ChangeLog
lisp/progmodes/python.el
test/ChangeLog
test/automated/python-tests.el

index a2467e6b00f408830e59f0217055509fba024339..b3bedaaffa98d9402a84c9fbb090b1f5faee0bb9 100644 (file)
@@ -1,3 +1,23 @@
+2015-01-26  Fabián Ezequiel Gallina  <fgallina@gnu.org>
+
+       python.el: New non-global state dependent indentation engine.
+       (Bug#18319, Bug#19595)
+
+       * progmodes/python.el (python-syntax-comment-or-string-p): Accept
+       PPSS as argument.
+       (python-syntax-closing-paren-p): New function.
+       (python-indent-current-level)
+       (python-indent-levels): Mark obsolete.
+       (python-indent-context): Return more context cases.
+       (python-indent--calculate-indentation)
+       (python-indent--calculate-levels): New functions.
+       (python-indent-calculate-levels): Use them.
+       (python-indent-calculate-indentation, python-indent-line):
+       (python-indent-line-function): Rewritten to use new API.
+       (python-indent-dedent-line): Simplify logic.
+       (python-indent-dedent-line-backspace): Use `unless`.
+       (python-indent-toggle-levels): Delete function.
+
 2015-01-22  Wolfgang Jenkner  <wjenkner@inode.at>
 
        * calc/calc-units.el (math-units-in-expr-p)
index 833c3d9a9c50545a217682f65e42a889af7e809f..d0a83087554d77d0cfbda490d58affc8e1a52d18 100644 (file)
@@ -447,9 +447,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")
@@ -671,10 +676,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'.")
+  "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."
@@ -714,356 +737,358 @@ It makes underscores and dots word constituent chars.")
                      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."
-  (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
-           (goto-char context-start)
-           (current-indentation))
-          ;; 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))
-             (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))))
-  (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)"
-  (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-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-marker) (save-excursion
-                                  (back-to-indentation)
-                                  (point-marker)))
-             (> (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)
index 11bf34d901c1fd57f72747b02a561f8259cd7ac9..62876e9ecae1c87dfb14e523ac4cbcff771a1254 100644 (file)
@@ -1,3 +1,25 @@
+2015-01-26  Fabián Ezequiel Gallina  <fgallina@gnu.org>
+
+       * automated/python-tests.el (python-indent-pep8-1)
+       (python-indent-pep8-2, python-indent-pep8-3)
+       (python-indent-after-comment-1, python-indent-after-comment-2)
+       (python-indent-inside-paren-1, python-indent-inside-paren-2)
+       (python-indent-after-block-1, python-indent-after-block-2)
+       (python-indent-after-backslash-1, python-indent-after-backslash-2)
+       (python-indent-after-backslash-3, python-indent-block-enders-1)
+       (python-indent-block-enders-2, python-indent-block-enders-3)
+       (python-indent-block-enders-4, python-indent-block-enders-5)
+       (python-indent-dedenters-1, python-indent-dedenters-2)
+       (python-indent-dedenters-3, python-indent-dedenters-4)
+       (python-indent-dedenters-5, python-indent-dedenters-6)
+       (python-indent-dedenters-7, python-indent-dedenters-8): Fix tests.
+       (python-indent-base-case, python-indent-after-block-3)
+       (python-indent-after-backslash-5, python-indent-inside-paren-3)
+       (python-indent-inside-paren-4, python-indent-inside-paren-5)
+       (python-indent-inside-paren-6, python-indent-inside-string-1)
+       (python-indent-inside-string-2, python-indent-inside-string-3)
+       (python-indent-dedent-line-backspace-1): New Tests.
+
 2015-01-24  Glenn Morris  <rgm@gnu.org>
 
        * automated/regexp-tests.el: Require regexp-opt, which is
index 35a7b99e4168da4021dd402131b4fbb7535ce8d7..5b77a1db49a79321445178e3d895fd884286b4fe 100644 (file)
@@ -174,13 +174,13 @@ aliqua."
 foo = long_function_name(var_one, var_two,
                          var_three, var_four)
 "
-   (should (eq (car (python-indent-context)) 'no-indent))
+   (should (eq (car (python-indent-context)) :no-indent))
    (should (= (python-indent-calculate-indentation) 0))
    (python-tests-look-at "foo = long_function_name(var_one, var_two,")
-   (should (eq (car (python-indent-context)) 'after-line))
+   (should (eq (car (python-indent-context)) :after-line))
    (should (= (python-indent-calculate-indentation) 0))
    (python-tests-look-at "var_three, var_four)")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren))
    (should (= (python-indent-calculate-indentation) 25))))
 
 (ert-deftest python-indent-pep8-2 ()
@@ -192,19 +192,22 @@ def long_function_name(
         var_four):
     print (var_one)
 "
-   (should (eq (car (python-indent-context)) 'no-indent))
+   (should (eq (car (python-indent-context)) :no-indent))
    (should (= (python-indent-calculate-indentation) 0))
    (python-tests-look-at "def long_function_name(")
-   (should (eq (car (python-indent-context)) 'after-line))
+   (should (eq (car (python-indent-context)) :after-line))
    (should (= (python-indent-calculate-indentation) 0))
    (python-tests-look-at "var_one, var_two, var_three,")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context))
+               :inside-paren-newline-start-from-block))
    (should (= (python-indent-calculate-indentation) 8))
    (python-tests-look-at "var_four):")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context))
+               :inside-paren-newline-start-from-block))
    (should (= (python-indent-calculate-indentation) 8))
    (python-tests-look-at "print (var_one)")
-   (should (eq (car (python-indent-context)) 'after-beginning-of-block))
+   (should (eq (car (python-indent-context))
+               :after-block-start))
    (should (= (python-indent-calculate-indentation) 4))))
 
 (ert-deftest python-indent-pep8-3 ()
@@ -215,18 +218,34 @@ foo = long_function_name(
   var_one, var_two,
   var_three, var_four)
 "
-   (should (eq (car (python-indent-context)) 'no-indent))
+   (should (eq (car (python-indent-context)) :no-indent))
    (should (= (python-indent-calculate-indentation) 0))
    (python-tests-look-at "foo = long_function_name(")
-   (should (eq (car (python-indent-context)) 'after-line))
+   (should (eq (car (python-indent-context)) :after-line))
    (should (= (python-indent-calculate-indentation) 0))
    (python-tests-look-at "var_one, var_two,")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren-newline-start))
    (should (= (python-indent-calculate-indentation) 4))
    (python-tests-look-at "var_three, var_four)")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren-newline-start))
    (should (= (python-indent-calculate-indentation) 4))))
 
+(ert-deftest python-indent-base-case ()
+  "Check base case does not trigger errors."
+  (python-tests-with-temp-buffer
+   "
+
+"
+   (goto-char (point-min))
+   (should (eq (car (python-indent-context)) :no-indent))
+   (should (= (python-indent-calculate-indentation) 0))
+   (forward-line 1)
+   (should (eq (car (python-indent-context)) :after-line))
+   (should (= (python-indent-calculate-indentation) 0))
+   (forward-line 1)
+   (should (eq (car (python-indent-context)) :after-line))
+   (should (= (python-indent-calculate-indentation) 0))))
+
 (ert-deftest python-indent-after-comment-1 ()
   "The most simple after-comment case that shouldn't fail."
   (python-tests-with-temp-buffer
@@ -240,23 +259,23 @@ class Blag(object):
 # with the exception with which the first child failed.
 "
    (python-tests-look-at "# We only complete")
-   (should (eq (car (python-indent-context)) 'after-line))
+   (should (eq (car (python-indent-context)) :after-block-end))
    (should (= (python-indent-calculate-indentation) 8))
    (python-tests-look-at "# terminal state")
-   (should (eq (car (python-indent-context)) 'after-comment))
+   (should (eq (car (python-indent-context)) :after-comment))
    (should (= (python-indent-calculate-indentation) 8))
    (python-tests-look-at "# with the exception")
-   (should (eq (car (python-indent-context)) 'after-comment))
+   (should (eq (car (python-indent-context)) :after-comment))
    ;; This one indents relative to previous block, even given the fact
    ;; that it was under-indented.
    (should (= (python-indent-calculate-indentation) 4))
    (python-tests-look-at "# terminal state" -1)
    ;; It doesn't hurt to check again.
-   (should (eq (car (python-indent-context)) 'after-comment))
+   (should (eq (car (python-indent-context)) :after-comment))
    (python-indent-line)
    (should (= (current-indentation) 8))
    (python-tests-look-at "# with the exception")
-   (should (eq (car (python-indent-context)) 'after-comment))
+   (should (eq (car (python-indent-context)) :after-comment))
    ;; Now everything should be lined up.
    (should (= (python-indent-calculate-indentation) 8))))
 
@@ -275,33 +294,33 @@ now_we_do_mess_cause_this_is_not_a_comment = 1
 # yeah, that.
 "
    (python-tests-look-at "# I don't do much")
-   (should (eq (car (python-indent-context)) 'after-beginning-of-block))
+   (should (eq (car (python-indent-context)) :after-block-start))
    (should (= (python-indent-calculate-indentation) 4))
    (python-tests-look-at "return arg")
    ;; Comment here just gets ignored, this line is not a comment so
    ;; the rules won't apply here.
-   (should (eq (car (python-indent-context)) 'after-beginning-of-block))
+   (should (eq (car (python-indent-context)) :after-block-start))
    (should (= (python-indent-calculate-indentation) 4))
    (python-tests-look-at "# This comment is badly")
-   (should (eq (car (python-indent-context)) 'after-line))
+   (should (eq (car (python-indent-context)) :after-block-end))
    ;; The return keyword moves indentation backwards 4 spaces, but
    ;; let's assume this comment was placed there because the user
    ;; wanted to (manually adding spaces or whatever).
    (should (= (python-indent-calculate-indentation) 0))
    (python-tests-look-at "# but we won't mess")
-   (should (eq (car (python-indent-context)) 'after-comment))
+   (should (eq (car (python-indent-context)) :after-comment))
    (should (= (python-indent-calculate-indentation) 4))
    ;; Behave the same for blank lines: potentially a comment.
    (forward-line 1)
-   (should (eq (car (python-indent-context)) 'after-comment))
+   (should (eq (car (python-indent-context)) :after-comment))
    (should (= (python-indent-calculate-indentation) 4))
    (python-tests-look-at "now_we_do_mess")
    ;; Here is where comment indentation starts to get ignored and
    ;; where the user can't freely indent anymore.
-   (should (eq (car (python-indent-context)) 'after-line))
+   (should (eq (car (python-indent-context)) :after-block-end))
    (should (= (python-indent-calculate-indentation) 0))
    (python-tests-look-at "# yeah, that.")
-   (should (eq (car (python-indent-context)) 'after-line))
+   (should (eq (car (python-indent-context)) :after-line))
    (should (= (python-indent-calculate-indentation) 0))))
 
 (ert-deftest python-indent-inside-paren-1 ()
@@ -325,49 +344,53 @@ data = {
 }
 "
    (python-tests-look-at "data = {")
-   (should (eq (car (python-indent-context)) 'after-line))
+   (should (eq (car (python-indent-context)) :after-line))
    (should (= (python-indent-calculate-indentation) 0))
    (python-tests-look-at "'key':")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren-newline-start))
    (should (= (python-indent-calculate-indentation) 4))
    (python-tests-look-at "{")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren-newline-start))
    (should (= (python-indent-calculate-indentation) 4))
    (python-tests-look-at "'objlist': [")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren-newline-start))
    (should (= (python-indent-calculate-indentation) 8))
    (python-tests-look-at "{")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren-newline-start))
    (should (= (python-indent-calculate-indentation) 12))
    (python-tests-look-at "'pk': 1,")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren-newline-start))
    (should (= (python-indent-calculate-indentation) 16))
    (python-tests-look-at "'name': 'first',")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren-newline-start))
    (should (= (python-indent-calculate-indentation) 16))
    (python-tests-look-at "},")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context))
+               :inside-paren-at-closing-nested-paren))
    (should (= (python-indent-calculate-indentation) 12))
    (python-tests-look-at "{")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren-newline-start))
    (should (= (python-indent-calculate-indentation) 12))
    (python-tests-look-at "'pk': 2,")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren-newline-start))
    (should (= (python-indent-calculate-indentation) 16))
    (python-tests-look-at "'name': 'second',")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren-newline-start))
    (should (= (python-indent-calculate-indentation) 16))
    (python-tests-look-at "}")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context))
+               :inside-paren-at-closing-nested-paren))
    (should (= (python-indent-calculate-indentation) 12))
    (python-tests-look-at "]")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context))
+               :inside-paren-at-closing-nested-paren))
    (should (= (python-indent-calculate-indentation) 8))
    (python-tests-look-at "}")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context))
+               :inside-paren-at-closing-nested-paren))
    (should (= (python-indent-calculate-indentation) 4))
    (python-tests-look-at "}")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren-at-closing-paren))
    (should (= (python-indent-calculate-indentation) 0))))
 
 (ert-deftest python-indent-inside-paren-2 ()
@@ -384,43 +407,121 @@ data = {'key': {
 }}
 "
    (python-tests-look-at "data = {")
-   (should (eq (car (python-indent-context)) 'after-line))
+   (should (eq (car (python-indent-context)) :after-line))
    (should (= (python-indent-calculate-indentation) 0))
    (python-tests-look-at "'objlist': [")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren-newline-start))
    (should (= (python-indent-calculate-indentation) 4))
    (python-tests-look-at "{'pk': 1,")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren-newline-start))
    (should (= (python-indent-calculate-indentation) 8))
    (python-tests-look-at "'name': 'first'},")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren))
    (should (= (python-indent-calculate-indentation) 9))
    (python-tests-look-at "{'pk': 2,")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren-newline-start))
    (should (= (python-indent-calculate-indentation) 8))
    (python-tests-look-at "'name': 'second'}")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren))
    (should (= (python-indent-calculate-indentation) 9))
    (python-tests-look-at "]")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context))
+               :inside-paren-at-closing-nested-paren))
    (should (= (python-indent-calculate-indentation) 4))
    (python-tests-look-at "}}")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context))
+               :inside-paren-at-closing-nested-paren))
    (should (= (python-indent-calculate-indentation) 0))
    (python-tests-look-at "}")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren-at-closing-paren))
    (should (= (python-indent-calculate-indentation) 0))))
 
+(ert-deftest python-indent-inside-paren-3 ()
+  "The simplest case possible."
+  (python-tests-with-temp-buffer
+   "
+data = ('these',
+        'are',
+        'the',
+        'tokens')
+"
+   (python-tests-look-at "data = ('these',")
+   (should (eq (car (python-indent-context)) :after-line))
+   (should (= (python-indent-calculate-indentation) 0))
+   (forward-line 1)
+   (should (eq (car (python-indent-context)) :inside-paren))
+   (should (= (python-indent-calculate-indentation) 8))
+   (forward-line 1)
+   (should (eq (car (python-indent-context)) :inside-paren))
+   (should (= (python-indent-calculate-indentation) 8))
+   (forward-line 1)
+   (should (eq (car (python-indent-context)) :inside-paren))
+   (should (= (python-indent-calculate-indentation) 8))))
+
+(ert-deftest python-indent-inside-paren-4 ()
+  "Respect indentation of first column."
+  (python-tests-with-temp-buffer
+   "
+data = [ [ 'these', 'are'],
+         ['the', 'tokens' ] ]
+"
+   (python-tests-look-at "data = [ [ 'these', 'are'],")
+   (should (eq (car (python-indent-context)) :after-line))
+   (should (= (python-indent-calculate-indentation) 0))
+   (forward-line 1)
+   (should (eq (car (python-indent-context)) :inside-paren))
+   (should (= (python-indent-calculate-indentation) 9))))
+
+(ert-deftest python-indent-inside-paren-5 ()
+  "Test when :inside-paren initial parens are skipped in context start."
+  (python-tests-with-temp-buffer
+   "
+while ((not some_condition) and
+       another_condition):
+    do_something_interesting(
+        with_some_arg)
+"
+   (python-tests-look-at "while ((not some_condition) and")
+   (should (eq (car (python-indent-context)) :after-line))
+   (should (= (python-indent-calculate-indentation) 0))
+   (forward-line 1)
+   (should (eq (car (python-indent-context)) :inside-paren))
+   (should (= (python-indent-calculate-indentation) 7))
+   (forward-line 1)
+   (should (eq (car (python-indent-context)) :after-block-start))
+   (should (= (python-indent-calculate-indentation) 4))
+   (forward-line 1)
+   (should (eq (car (python-indent-context)) :inside-paren-newline-start))
+   (should (= (python-indent-calculate-indentation) 8))))
+
+(ert-deftest python-indent-inside-paren-6 ()
+  "This should be aligned.."
+  (python-tests-with-temp-buffer
+   "
+CHOICES = (('some', 'choice'),
+           ('another', 'choice'),
+           ('more', 'choices'))
+"
+   (python-tests-look-at "CHOICES = (('some', 'choice'),")
+   (should (eq (car (python-indent-context)) :after-line))
+   (should (= (python-indent-calculate-indentation) 0))
+   (forward-line 1)
+   (should (eq (car (python-indent-context)) :inside-paren))
+   (should (= (python-indent-calculate-indentation) 11))
+   (forward-line 1)
+   (should (eq (car (python-indent-context)) :inside-paren))
+   (should (= (python-indent-calculate-indentation) 11))))
+
 (ert-deftest python-indent-after-block-1 ()
   "The most simple after-block case that shouldn't fail."
   (python-tests-with-temp-buffer
    "
 def foo(a, b, c=True):
 "
-   (should (eq (car (python-indent-context)) 'no-indent))
+   (should (eq (car (python-indent-context)) :no-indent))
    (should (= (python-indent-calculate-indentation) 0))
    (goto-char (point-max))
-   (should (eq (car (python-indent-context)) 'after-beginning-of-block))
+   (should (eq (car (python-indent-context)) :after-block-start))
    (should (= (python-indent-calculate-indentation) 4))))
 
 (ert-deftest python-indent-after-block-2 ()
@@ -432,9 +533,28 @@ def foo(a, b, c={
 }):
 "
    (goto-char (point-max))
-   (should (eq (car (python-indent-context)) 'after-beginning-of-block))
+   (should (eq (car (python-indent-context)) :after-block-start))
    (should (= (python-indent-calculate-indentation) 4))))
 
+(ert-deftest python-indent-after-block-3 ()
+  "A weird (malformed) sample, usually found in python shells."
+  (python-tests-with-temp-buffer
+   "
+In [1]:
+def func():
+pass
+
+In [2]:
+something
+"
+   (python-tests-look-at "pass")
+   (should (eq (car (python-indent-context)) :after-block-start))
+   (should (= (python-indent-calculate-indentation) 4))
+   (python-tests-look-at "something")
+   (end-of-line)
+   (should (eq (car (python-indent-context)) :after-line))
+   (should (= (python-indent-calculate-indentation) 0))))
+
 (ert-deftest python-indent-after-backslash-1 ()
   "The most common case."
   (python-tests-with-temp-buffer
@@ -444,16 +564,16 @@ from foo.bar.baz import something, something_1 \\\\
     something_4, something_5
 "
    (python-tests-look-at "from foo.bar.baz import something, something_1")
-   (should (eq (car (python-indent-context)) 'after-line))
+   (should (eq (car (python-indent-context)) :after-line))
    (should (= (python-indent-calculate-indentation) 0))
    (python-tests-look-at "something_2 something_3,")
-   (should (eq (car (python-indent-context)) 'after-backslash))
+   (should (eq (car (python-indent-context)) :after-backslash-first-line))
    (should (= (python-indent-calculate-indentation) 4))
    (python-tests-look-at "something_4, something_5")
-   (should (eq (car (python-indent-context)) 'after-backslash))
+   (should (eq (car (python-indent-context)) :after-backslash))
    (should (= (python-indent-calculate-indentation) 4))
    (goto-char (point-max))
-   (should (eq (car (python-indent-context)) 'after-line))
+   (should (eq (car (python-indent-context)) :after-line))
    (should (= (python-indent-calculate-indentation) 0))))
 
 (ert-deftest python-indent-after-backslash-2 ()
@@ -471,40 +591,104 @@ objects = Thing.objects.all() \\\\
                        .values_list()
 "
    (python-tests-look-at "objects = Thing.objects.all()")
-   (should (eq (car (python-indent-context)) 'after-line))
+   (should (eq (car (python-indent-context)) :after-line))
    (should (= (python-indent-calculate-indentation) 0))
    (python-tests-look-at ".filter(")
-   (should (eq (car (python-indent-context)) 'after-backslash))
+   (should (eq (car (python-indent-context))
+               :after-backslash-dotted-continuation))
    (should (= (python-indent-calculate-indentation) 23))
    (python-tests-look-at "type='toy',")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren-newline-start))
    (should (= (python-indent-calculate-indentation) 27))
    (python-tests-look-at "status='bought'")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren-newline-start))
    (should (= (python-indent-calculate-indentation) 27))
    (python-tests-look-at ") \\\\")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren-at-closing-paren))
    (should (= (python-indent-calculate-indentation) 23))
    (python-tests-look-at ".aggregate(")
-   (should (eq (car (python-indent-context)) 'after-backslash))
+   (should (eq (car (python-indent-context))
+               :after-backslash-dotted-continuation))
    (should (= (python-indent-calculate-indentation) 23))
    (python-tests-look-at "Sum('amount')")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren-newline-start))
    (should (= (python-indent-calculate-indentation) 27))
    (python-tests-look-at ") \\\\")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren-at-closing-paren))
    (should (= (python-indent-calculate-indentation) 23))
    (python-tests-look-at ".values_list()")
-   (should (eq (car (python-indent-context)) 'after-backslash))
+   (should (eq (car (python-indent-context))
+               :after-backslash-dotted-continuation))
    (should (= (python-indent-calculate-indentation) 23))
    (forward-line 1)
-   (should (eq (car (python-indent-context)) 'after-line))
+   (should (eq (car (python-indent-context)) :after-line))
    (should (= (python-indent-calculate-indentation) 0))))
 
+(ert-deftest python-indent-after-backslash-3 ()
+  "Backslash continuation from block start."
+  (python-tests-with-temp-buffer
+   "
+with open('/path/to/some/file/you/want/to/read') as file_1, \\\\
+     open('/path/to/some/file/being/written', 'w') as file_2:
+    file_2.write(file_1.read())
+"
+   (python-tests-look-at
+    "with open('/path/to/some/file/you/want/to/read') as file_1, \\\\")
+   (should (eq (car (python-indent-context)) :after-line))
+   (should (= (python-indent-calculate-indentation) 0))
+   (python-tests-look-at
+    "open('/path/to/some/file/being/written', 'w') as file_2")
+   (should (eq (car (python-indent-context))
+               :after-backslash-block-continuation))
+   (should (= (python-indent-calculate-indentation) 5))
+   (python-tests-look-at "file_2.write(file_1.read())")
+   (should (eq (car (python-indent-context)) :after-block-start))
+   (should (= (python-indent-calculate-indentation) 4))))
+
+(ert-deftest python-indent-after-backslash-4 ()
+  "Backslash continuation from assignment."
+  (python-tests-with-temp-buffer
+   "
+super_awful_assignment = some_calculation() and \\\\
+                         another_calculation() and \\\\
+                         some_final_calculation()
+"
+   (python-tests-look-at
+    "super_awful_assignment = some_calculation() and \\\\")
+   (should (eq (car (python-indent-context)) :after-line))
+   (should (= (python-indent-calculate-indentation) 0))
+   (python-tests-look-at "another_calculation() and \\\\")
+   (should (eq (car (python-indent-context))
+               :after-backslash-assignment-continuation))
+   (should (= (python-indent-calculate-indentation) 25))
+   (python-tests-look-at "some_final_calculation()")
+   (should (eq (car (python-indent-context)) :after-backslash))
+   (should (= (python-indent-calculate-indentation) 25))))
+
+(ert-deftest python-indent-after-backslash-5 ()
+  "Dotted continuation bizarre example."
+  (python-tests-with-temp-buffer
+   "
+def delete_all_things():
+    Thing \\\\
+        .objects.all() \\\\
+                .delete()
+"
+   (python-tests-look-at "Thing \\\\")
+   (should (eq (car (python-indent-context)) :after-block-start))
+   (should (= (python-indent-calculate-indentation) 4))
+   (python-tests-look-at ".objects.all() \\\\")
+   (should (eq (car (python-indent-context)) :after-backslash-first-line))
+   (should (= (python-indent-calculate-indentation) 8))
+   (python-tests-look-at ".delete()")
+   (should (eq (car (python-indent-context))
+               :after-backslash-dotted-continuation))
+   (should (= (python-indent-calculate-indentation) 16))))
+
 (ert-deftest python-indent-block-enders-1 ()
   "Test de-indentation for pass keyword."
   (python-tests-with-temp-buffer
-      "
+   "
 Class foo(object):
 
     def bar(self):
@@ -516,17 +700,18 @@ Class foo(object):
         else:
             pass
 "
-    (python-tests-look-at "3)")
-    (forward-line 1)
-    (should (= (python-indent-calculate-indentation) 8))
-    (python-tests-look-at "pass")
-    (forward-line 1)
-    (should (= (python-indent-calculate-indentation) 8))))
+   (python-tests-look-at "3)")
+   (forward-line 1)
+   (should (= (python-indent-calculate-indentation) 8))
+   (python-tests-look-at "pass")
+   (forward-line 1)
+   (should (eq (car (python-indent-context)) :after-block-end))
+   (should (= (python-indent-calculate-indentation) 8))))
 
 (ert-deftest python-indent-block-enders-2 ()
   "Test de-indentation for return keyword."
   (python-tests-with-temp-buffer
-      "
+   "
 Class foo(object):
     '''raise lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do
 
@@ -539,64 +724,68 @@ Class foo(object):
                     2,
                     3)
 "
-    (python-tests-look-at "def")
-    (should (= (python-indent-calculate-indentation) 4))
-    (python-tests-look-at "if")
-    (should (= (python-indent-calculate-indentation) 8))
-    (python-tests-look-at "return")
-    (should (= (python-indent-calculate-indentation) 12))
-    (goto-char (point-max))
-    (should (= (python-indent-calculate-indentation) 8))))
+   (python-tests-look-at "def")
+   (should (= (python-indent-calculate-indentation) 4))
+   (python-tests-look-at "if")
+   (should (= (python-indent-calculate-indentation) 8))
+   (python-tests-look-at "return")
+   (should (= (python-indent-calculate-indentation) 12))
+   (goto-char (point-max))
+   (should (eq (car (python-indent-context)) :after-block-end))
+   (should (= (python-indent-calculate-indentation) 8))))
 
 (ert-deftest python-indent-block-enders-3 ()
   "Test de-indentation for continue keyword."
   (python-tests-with-temp-buffer
-      "
+   "
 for element in lst:
     if element is None:
         continue
 "
-    (python-tests-look-at "if")
-    (should (= (python-indent-calculate-indentation) 4))
-    (python-tests-look-at "continue")
-    (should (= (python-indent-calculate-indentation) 8))
-    (forward-line 1)
-    (should (= (python-indent-calculate-indentation) 4))))
+   (python-tests-look-at "if")
+   (should (= (python-indent-calculate-indentation) 4))
+   (python-tests-look-at "continue")
+   (should (= (python-indent-calculate-indentation) 8))
+   (forward-line 1)
+   (should (eq (car (python-indent-context)) :after-block-end))
+   (should (= (python-indent-calculate-indentation) 4))))
 
 (ert-deftest python-indent-block-enders-4 ()
   "Test de-indentation for break keyword."
   (python-tests-with-temp-buffer
-      "
+   "
 for element in lst:
     if element is None:
         break
 "
-    (python-tests-look-at "if")
-    (should (= (python-indent-calculate-indentation) 4))
-    (python-tests-look-at "break")
-    (should (= (python-indent-calculate-indentation) 8))
-    (forward-line 1)
-    (should (= (python-indent-calculate-indentation) 4))))
+   (python-tests-look-at "if")
+   (should (= (python-indent-calculate-indentation) 4))
+   (python-tests-look-at "break")
+   (should (= (python-indent-calculate-indentation) 8))
+   (forward-line 1)
+   (should (eq (car (python-indent-context)) :after-block-end))
+   (should (= (python-indent-calculate-indentation) 4))))
 
 (ert-deftest python-indent-block-enders-5 ()
   "Test de-indentation for raise keyword."
   (python-tests-with-temp-buffer
-      "
+   "
 for element in lst:
     if element is None:
         raise ValueError('Element cannot be None')
 "
-    (python-tests-look-at "if")
-    (should (= (python-indent-calculate-indentation) 4))
-    (python-tests-look-at "raise")
-    (should (= (python-indent-calculate-indentation) 8))
-    (forward-line 1)
-    (should (= (python-indent-calculate-indentation) 4))))
+   (python-tests-look-at "if")
+   (should (= (python-indent-calculate-indentation) 4))
+   (python-tests-look-at "raise")
+   (should (= (python-indent-calculate-indentation) 8))
+   (forward-line 1)
+   (should (eq (car (python-indent-context)) :after-block-end))
+   (should (= (python-indent-calculate-indentation) 4))))
 
 (ert-deftest python-indent-dedenters-1 ()
   "Test de-indentation for the elif keyword."
   (python-tests-with-temp-buffer
-      "
+   "
 if save:
     try:
         write_to_disk(data)
@@ -604,15 +793,15 @@ if save:
         cleanup()
         elif
 "
-    (python-tests-look-at "elif\n")
-    (should (eq (car (python-indent-context)) 'dedenter-statement))
-    (should (= (python-indent-calculate-indentation) 0))
-    (should (equal (python-indent-calculate-levels) '(0)))))
+   (python-tests-look-at "elif\n")
+   (should (eq (car (python-indent-context)) :at-dedenter-block-start))
+   (should (= (python-indent-calculate-indentation) 0))
+   (should (= (python-indent-calculate-indentation t) 0))))
 
 (ert-deftest python-indent-dedenters-2 ()
   "Test de-indentation for the else keyword."
   (python-tests-with-temp-buffer
-      "
+   "
 if save:
     try:
         write_to_disk(data)
@@ -627,43 +816,50 @@ if save:
     finally:
         data.free()
 "
-    (python-tests-look-at "else\n")
-    (should (eq (car (python-indent-context)) 'dedenter-statement))
-    (should (= (python-indent-calculate-indentation) 8))
-    (should (equal (python-indent-calculate-levels) '(0 4 8)))))
+   (python-tests-look-at "else\n")
+   (should (eq (car (python-indent-context)) :at-dedenter-block-start))
+   (should (= (python-indent-calculate-indentation) 8))
+   (python-indent-line t)
+   (should (= (python-indent-calculate-indentation t) 4))
+   (python-indent-line t)
+   (should (= (python-indent-calculate-indentation t) 0))
+   (python-indent-line t)
+   (should (= (python-indent-calculate-indentation t) 8))))
 
 (ert-deftest python-indent-dedenters-3 ()
   "Test de-indentation for the except keyword."
   (python-tests-with-temp-buffer
-      "
+   "
 if save:
     try:
         write_to_disk(data)
         except
 "
-    (python-tests-look-at "except\n")
-    (should (eq (car (python-indent-context)) 'dedenter-statement))
-    (should (= (python-indent-calculate-indentation) 4))
-    (should (equal (python-indent-calculate-levels) '(4)))))
+   (python-tests-look-at "except\n")
+   (should (eq (car (python-indent-context)) :at-dedenter-block-start))
+   (should (= (python-indent-calculate-indentation) 4))
+   (python-indent-line t)
+   (should (= (python-indent-calculate-indentation t) 4))))
 
 (ert-deftest python-indent-dedenters-4 ()
   "Test de-indentation for the finally keyword."
   (python-tests-with-temp-buffer
-      "
+   "
 if save:
     try:
         write_to_disk(data)
         finally
 "
-    (python-tests-look-at "finally\n")
-    (should (eq (car (python-indent-context)) 'dedenter-statement))
-    (should (= (python-indent-calculate-indentation) 4))
-    (should (equal (python-indent-calculate-levels) '(4)))))
+   (python-tests-look-at "finally\n")
+   (should (eq (car (python-indent-context)) :at-dedenter-block-start))
+   (should (= (python-indent-calculate-indentation) 4))
+   (python-indent-line t)
+   (should (= (python-indent-calculate-indentation) 4))))
 
 (ert-deftest python-indent-dedenters-5 ()
   "Test invalid levels are skipped in a complex example."
   (python-tests-with-temp-buffer
-      "
+   "
 if save:
     try:
         write_to_disk(data)
@@ -676,29 +872,31 @@ if save:
             do_cleanup()
         else
 "
-    (python-tests-look-at "else\n")
-    (should (eq (car (python-indent-context)) 'dedenter-statement))
-    (should (= (python-indent-calculate-indentation) 8))
-    (should (equal (python-indent-calculate-levels) '(0 8)))))
+   (python-tests-look-at "else\n")
+   (should (eq (car (python-indent-context)) :at-dedenter-block-start))
+   (should (= (python-indent-calculate-indentation) 8))
+   (should (= (python-indent-calculate-indentation t) 0))
+   (python-indent-line t)
+   (should (= (python-indent-calculate-indentation t) 8))))
 
 (ert-deftest python-indent-dedenters-6 ()
   "Test indentation is zero when no opening block for dedenter."
   (python-tests-with-temp-buffer
-      "
+   "
 try:
     # if save:
         write_to_disk(data)
         else
 "
-    (python-tests-look-at "else\n")
-    (should (eq (car (python-indent-context)) 'dedenter-statement))
-    (should (= (python-indent-calculate-indentation) 0))
-    (should (equal (python-indent-calculate-levels) '(0)))))
+   (python-tests-look-at "else\n")
+   (should (eq (car (python-indent-context)) :at-dedenter-block-start))
+   (should (= (python-indent-calculate-indentation) 0))
+   (should (= (python-indent-calculate-indentation t) 0))))
 
 (ert-deftest python-indent-dedenters-7 ()
   "Test indentation case from Bug#15163."
   (python-tests-with-temp-buffer
-      "
+   "
 if a:
     if b:
         pass
@@ -706,10 +904,10 @@ if a:
         pass
         else:
 "
-    (python-tests-look-at "else:" 2)
-    (should (eq (car (python-indent-context)) 'dedenter-statement))
-    (should (= (python-indent-calculate-indentation) 0))
-    (should (equal (python-indent-calculate-levels) '(0)))))
+   (python-tests-look-at "else:" 2)
+   (should (eq (car (python-indent-context)) :at-dedenter-block-start))
+   (should (= (python-indent-calculate-indentation) 0))
+   (should (= (python-indent-calculate-indentation t) 0))))
 
 (ert-deftest python-indent-dedenters-8 ()
   "Test indentation for Bug#18432."
@@ -721,10 +919,99 @@ if (a == 1 or
 elif (a == 3 or
 a == 4):
 "
+   (python-tests-look-at "elif (a == 3 or")
+   (should (eq (car (python-indent-context)) :at-dedenter-block-start))
+   (should (= (python-indent-calculate-indentation) 0))
+   (should (= (python-indent-calculate-indentation t) 0))
    (python-tests-look-at "a == 4):\n")
-   (should (eq (car (python-indent-context)) 'inside-paren))
+   (should (eq (car (python-indent-context)) :inside-paren))
    (should (= (python-indent-calculate-indentation) 6))
-   (should (equal (python-indent-calculate-levels) '(0 4 6)))))
+   (python-indent-line)
+   (should (= (python-indent-calculate-indentation t) 4))
+   (python-indent-line t)
+   (should (= (python-indent-calculate-indentation t) 0))
+   (python-indent-line t)
+   (should (= (python-indent-calculate-indentation t) 6))))
+
+(ert-deftest python-indent-inside-string-1 ()
+  "Test indentation for strings."
+  (python-tests-with-temp-buffer
+   "
+multiline = '''
+bunch
+of
+lines
+'''
+"
+   (python-tests-look-at "multiline = '''")
+   (should (eq (car (python-indent-context)) :after-line))
+   (should (= (python-indent-calculate-indentation) 0))
+   (python-tests-look-at "bunch")
+   (should (eq (car (python-indent-context)) :inside-string))
+   (should (= (python-indent-calculate-indentation) 0))
+   (python-tests-look-at "of")
+   (should (eq (car (python-indent-context)) :inside-string))
+   (should (= (python-indent-calculate-indentation) 0))
+   (python-tests-look-at "lines")
+   (should (eq (car (python-indent-context)) :inside-string))
+   (should (= (python-indent-calculate-indentation) 0))
+   (python-tests-look-at "'''")
+   (should (eq (car (python-indent-context)) :inside-string))
+   (should (= (python-indent-calculate-indentation) 0))))
+
+(ert-deftest python-indent-inside-string-2 ()
+  "Test indentation for docstrings."
+  (python-tests-with-temp-buffer
+   "
+def fn(a, b, c=True):
+    '''docstring
+    bunch
+    of
+    lines
+    '''
+"
+   (python-tests-look-at "'''docstring")
+   (should (eq (car (python-indent-context)) :after-block-start))
+   (should (= (python-indent-calculate-indentation) 4))
+   (python-tests-look-at "bunch")
+   (should (eq (car (python-indent-context)) :inside-string))
+   (should (= (python-indent-calculate-indentation) 4))
+   (python-tests-look-at "of")
+   (should (eq (car (python-indent-context)) :inside-string))
+   (should (= (python-indent-calculate-indentation) 4))
+   (python-tests-look-at "lines")
+   (should (eq (car (python-indent-context)) :inside-string))
+   (should (= (python-indent-calculate-indentation) 4))
+   (python-tests-look-at "'''")
+   (should (eq (car (python-indent-context)) :inside-string))
+   (should (= (python-indent-calculate-indentation) 4))))
+
+(ert-deftest python-indent-inside-string-3 ()
+  "Test indentation for nested strings."
+  (python-tests-with-temp-buffer
+   "
+def fn(a, b, c=True):
+    some_var = '''
+    bunch
+    of
+    lines
+    '''
+"
+   (python-tests-look-at "some_var = '''")
+   (should (eq (car (python-indent-context)) :after-block-start))
+   (should (= (python-indent-calculate-indentation) 4))
+   (python-tests-look-at "bunch")
+   (should (eq (car (python-indent-context)) :inside-string))
+   (should (= (python-indent-calculate-indentation) 4))
+   (python-tests-look-at "of")
+   (should (eq (car (python-indent-context)) :inside-string))
+   (should (= (python-indent-calculate-indentation) 4))
+   (python-tests-look-at "lines")
+   (should (eq (car (python-indent-context)) :inside-string))
+   (should (= (python-indent-calculate-indentation) 4))
+   (python-tests-look-at "'''")
+   (should (eq (car (python-indent-context)) :inside-string))
+   (should (= (python-indent-calculate-indentation) 4))))
 
 (ert-deftest python-indent-electric-colon-1 ()
   "Test indentation case from Bug#18228."