0

This is (yet) another question related to electric-indent-mode and Python, but since I couldn't find what I address here in the other ones, here we go.

I find the behavior of electric-indent-mode quite useful in general, but there is a minor thing that annoys me:

Say we have the following buffer, where | (pipe) represents the cursor position:

def example():
    if True:
        print("Electricity")

    |

With electric-indent-mode on, pressing RET results in the following state:

def example():
    if True:
        print("Electricity")


        |

The automatic indentation of the newline regards only the first non-empty (i.e., that consists not only of whitespace) previous line. However, what I would like is to have it take into account the cursor position of the previous line if the previous line consists only of whitespace (and after pressing enter, still remove the whitespace on the previous line afterwards if there was any):

def example():
    if True:
        print("Electricity")


    |

How can I tweak electric-indent-mode configurations to achieve the desired behavior? Conversely, what are the possible solutions (regardless of using electric-indent-mode or not)?

3 Answers3

1

Typing [control j] instead of RET should jump to the beginning of line when electric-indent-mode is on. If EIM is off, RET jumps to the BOL and C-j to indent.

Andreas Röhler
  • 1,894
  • 10
  • 10
  • Thank you @Andreas Röhler. The problem is, however, that I am looking for a solution that works regardless of the indentation level we are in (and that does not require one to memorize or switch between keys). I have updated my question with a slightly different example to make the point clearer. Maybe [this answer](https://emacs.stackexchange.com/a/48858/13589) has some hints on how to solve it btw. – Arthur Colombini Gusmão Oct 17 '19 at 19:11
  • 2
    @ArthurColombiniGusmão Don't understand saying "to have it take into account the indentation of the previous line, even if it was an empty line" - how could an empty line have an indentation? – Andreas Röhler Oct 18 '19 at 06:49
  • sorry, I should have been more clear. By that I mean a line composed only of whitespace (e.g., spaces or tabs). Basically what I want to achieve is to have Emacs preserve the indentation level I had in the previous line, even if there was no non-whitespace character in it. I will update my question. – Arthur Colombini Gusmão Oct 18 '19 at 12:32
  • 1
    @ArthurColombiniGusmão IMO that may disturb users, as whitespace commonly isn't visible. Emacs would indent then after rules which are hard to follow. – Andreas Röhler Oct 19 '19 at 17:03
  • indeed you have a point regarding whitespace usually being invisible. What would make sense then is to have Emacs preserve the *cursor* position of the previous line *if* the previous line was an empty line. This is what I think would be very useful (I will update my question again based on this). – Arthur Colombini Gusmão Jan 29 '20 at 13:29
  • 1
    @ArthurColombiniGusmão Okay, but that's like newline-and-indent is expected to behave. If not, please consider a bug-report - said as a non-electric user, as electricity more often breaks my workflow than it helps. – Andreas Röhler Jan 30 '20 at 08:04
1

I see the need for this every day. The current behavior is really annoying, to the point of distracting me from the flow of writing code productively.

I have entered the previous code:

def example():
    if True:
        print("Electricity")

Entering a newline lines up the next line with the beginning of the print statement. To start a new definition, I have to hit TAB three times to get the cursor to the left margin. Then I realize I want a blank line, so I hit newline. This puts the indent back at the "print" level. 3 more TABS, then enter a comment:

# The following def does ...

Hitting newline then indents the comment to the "print" level.

def example():
    if True:
        print("Electricity")

        # The following def does ...

Now, position the cursor at the comment's #, hit M-\ (or 3 tabs), and the comment is at the left margin. Move to next line, hit M-\ (or 3 tabs) to get to left margin, and type:

def foo():

hitting newline indents the "def" to the "print" level, leaving the preceding comment at the left margin.

def example():
    if True:
        print("Electricity")

# The following def does ...
        def foo():

Move to the next line, hit enough tab characters to get to the left margin, and we're ready to start typing the def's body. At this point, the indentation stays where I want it to be, unless, after modifying the def's header, I hit newline, which will indent the def back to the "print" level. This happens even if there is a multi-line comment block before the "def"!

So, that's at least 3 instances of unwanted indentation, along with 9 tabs to undo the unwanted behavior, namely, lining up the indent with the previous line, especially when the previous line has been indented correctly, manually, by me.

@ArthurColombiniGusmão suggests that this will disturb users. Sure, it might. But it's a lot less disturbing to have properly-indented lines un-indented multiple times. If I end up with an unexpected indentation because the previous line was indented for some other reason, I should be able to fix it (by hitting TAB enough times), and know that my "def" won't be unwantedly indented, and not have that work undone by hitting newline.

Chelmite
  • 121
  • 4
  • Do you have `electric-indent-inhibit` set to nil in your Python buffers? If so, that could be why. Personally I don't experience this behavior of electric-mode reindenting previous lines. – Arthur Colombini Gusmão May 30 '20 at 15:15
0

After investigating a related question, I designed a solution that fits me well so far. As electric mode automatically removes whitespace from the previous line, there is no way we could modify a Python-specific function to conform to our desired behavior. Hence, the solution I found was to advise electric-indent-post-self-insert-function. With a buffer-local variable that is set to true in the major modes we choose, I modified the behavior of this function to not remove whitespace from previous line and apply the previous indentation level IF the previous line is an empty line; if previous line not empty, then perform the default behavior. I hope this can help.

Here is the commit in my dotfiles, and below the code, adapted to be posted here by refactoring necessary functions from my dotfiles to a single place:

(defvar-local acg/electric-indent-newline-as-previous-if-blank nil
  "Buffer-local variable that indicates one wants to have
`electric-indent-post-self-insert-function' indent a newly
inserted line with the same indentation as the previous line, if
the previous line was a blank line. This variable is used in
`acg/advice--electric-indent-post-self-insert-function'.

Particularly, I find this behavior quite useful in Python, as
discussed in https://emacs.stackexchange.com/q/53153/13589")

(defun acg/line-empty-p (&optional pos)
  "Returns `t' if line (optionally, line at POS) is empty or
composed only of whitespace. Adapted from
https://emacs.stackexchange.com/a/16825/13589"
  (save-excursion
    (goto-char (or pos (point)))
    (= (current-indentation)
       (- (line-end-position) (line-beginning-position)))))

(defun acg/advice--electric-indent-post-self-insert-function (orig-fun)
  "Advice to be put around `electric-indent-post-self-insert-function';
see `acg/electric-indent-newline-as-previous-if-blank'."
  (let (pos prev-indent prev-line-blank-p)
    (if (and acg/electric-indent-newline-as-previous-if-blank
             (save-excursion
               (previous-line)
               (setq prev-line-blank-p (acg/line-empty-p))))
        ;; Section below is part of the original function that I adapted
        (when (and electric-indent-mode
                   ;; Don't reindent while inserting spaces at beginning of line.
                   (or (not (memq last-command-event '(?\s ?\t)))
                       (save-excursion (skip-chars-backward " \t") (not (bolp))))
                   (setq pos (electric--after-char-pos))
                   (save-excursion
                     (goto-char pos)
                     (let ((act (or (run-hook-with-args-until-success
                                     'electric-indent-functions
                                     last-command-event)
                                    (memq last-command-event electric-indent-chars))))
                       (not
                        (or (memq act '(nil no-indent))
                            ;; In a string or comment.
                            (unless (eq act 'do-indent) (nth 8 (syntax-ppss))))))))
          ;; Get value of previous indentation
          (save-excursion
            (previous-line)
            (setq prev-indent (current-indentation)))
          ;; Indent current line catching errors
          (let ((at-newline (<= pos (line-beginning-position))))
            (unless (and electric-indent-inhibit
                         (not at-newline))
              (condition-case-unless-debug ()
                  ;; (indent-according-to-mode)
                  (indent-line-to prev-indent)
                (error (throw 'indent-error nil))))))
      ;; If not using modification or not blank line above, back to default
      (funcall orig-fun))))


(advice-add 'electric-indent-post-self-insert-function :around #'acg/advice--electric-indent-post-self-insert-function)
;; (advice-remove 'electric-indent-post-self-insert-function #'acg/advice--electric-indent-post-self-insert-function)

(add-hook
 'python-mode-hook
 (lambda () (setq acg/electric-indent-newline-as-previous-if-blank t)))