More Emacs Python mode: support for continuation lines

Tim Peters (tim@ksr.com)
Wed, 05 Feb 92 04:48:38 EST

The default pattern is like:

if a_really_long_name == an_extraordinarily_long_name and \
x < y < z < a < b < c < d < e < f < g < h < i and \
not len(sys.argv) == 3:
do_some_stuff()
etc()

Change vrbl py-continuation-offset in your .emacs to something you like
better (it defaults to 2; it's added to py-indent-offset to determine
the indentation of the 1st continuation line).

Be warned that there *isn't* a single value you'll like in all
circumstances. E.g., looking at the lib/*.py code, some multi-line
statements of Guido's used (in effect) a value of 8 for
py-continuation-offset, others 0, others 1, and there was even a -7 or
two (among others ...). It depends on what you're doing & on what looks
nice in context, things I won't pretend to be able to guess.

For that reason, you should view the indentation Python-mode supplies
for a continuation line as just a lazy (humble <grin>) default; change
it to something you like better, and the code will follow your lead
until the statement ends.

Whatever oddball indentation you use during a multi-line statement will
*not* affect the normal indentation of subsequent statements. So, e.g.,

animal_list = [ 'horse', \
'dog', \ user hand-fiddled the indent here
'cat', \ LFD automatically did it here
'python', ] and here
death = taxes and LFD knew that 0 indent is proper here

not-smart-but-accommodating-ly y'rs - tim

Tim Peters Kendall Square Research Corp
tim@ksr.com, ksr!tim@uunet.uu.net

ps: won't have time to hack Emacs the rest of the week, else I would
have held back on this. Back to the shadows ...

;; Major mode for editing Python programs.
;; by: Michael A. Guravage and Guido van Rossum <guido@cwi.nl>
;;
;; The following statements, placed in your .emacs file or site-init.el,
;; will cause this file to be autoloaded, and python-mode invoked, when
;; visiting .py files (assuming the file is in your load-path):
;;
;; (autoload 'python-mode "python-mode" "" t)
;; (setq auto-mode-alist
;; (cons '("\\.py$" . python-mode) auto-mode-alist))

;; Change log:
;
; Sun Feb 2 02:08:47 1992 tim
; added support for user-defined indentation increment:
; added python-indent variable
; changed py-indent-line to indent python-indent columns
; fixed small bug in py-indent-line (changed start of r.e.
; from [^#] to [^#\n])
; added py-delete-char function; bound to \177
; changed mode blurb accordingly
;
; Sun Feb 2 21:48:59 1992 tim
; renamed 'python-indent' to 'py-indent-offset' for internal consistency
; replaced regexp in py-indent-line so it no longer indents after
; lines like:
; a = b # ok: # found
; a = ': #'
;
; Mon Feb 3 20:37:27 1992 tim
; renamed file to 'python-mode.el' for consistency with other Emacs
; language modes; changed autoload instructions accordingly
; improved accuracy of new/changed docs
; changed py-python-command from defconst to defvar so .emacs can
; override it if desired
; add warning about indent-region; suggest indent-rigidly
;
; Wed Feb 5 03:23:31 1992 tim
; added support for auto-indenting of continuation lines:
; new vrbl py-continuation-offset
; new function py-continuation-line-p
; new function py-goto-initial-line
; new function py-compute-indentation
; rewrote py-indent-line
; changed py-indent-line to refrain from modifying the buffer if the
; indentation is already correct
; hid the hairy colon-line regexp in a const
; changed indent-region example to use legal Python
; documented all that

(provide 'python)

;; Constants and variables

(defvar py-python-command "python"
"*UNIX shell command used to start Python interpreter")

(defvar py-indent-offset 4
"*Indentation increment in Python mode")

(defvar py-continuation-offset 2
"*Indentation (in addition to py-indent-offset) for continued lines")

(defvar py-mode-map nil
"Keymap used in Python mode buffers")

(defvar py-mode-syntax-table nil
"Python mode syntax table")

(defconst py-colon-line-re
"\\([^#'\n]\\|'\\([^'\n\\]\\|\\\\.\\)*'\\)*:[ \t]*\\(#.*\\)?$"
"regexp matching lines opening a new block")

;; Initialize the keymap if it doesn't already exist

(if (null py-mode-map)
(progn
(setq py-mode-map (make-sparse-keymap))
(define-key py-mode-map "\C-c\C-c" 'py-execute-buffer)
(define-key py-mode-map "\C-c|" 'py-execute-region)
(define-key py-mode-map "\C-c!" 'py-shell)
(define-key py-mode-map "\177" 'py-delete-char)))

;; General Functions

(defun python-mode nil
"Major mode for editing Python files.

\\[py-execute-buffer] sends the entire buffer to the Python interpreter.
\\[py-execute-region] sends the current region.
\\[py-shell] starts a Python interpreter window; this will be used by
subsequent \\[py-execute-buffer] or \\[py-execute-region] commands.

Variable py-indent-offset is the indentation increment.

\\[newline-and-indent] indents by an extra py-indent-offset columns where
necessary. Use \\[py-delete-char] to reduce indentation.

\\[py-delete-char] reduces the indentation of a line by py-indent-offset columns
if point is at the first non-blank character (if any) of a line, or at
the last character of an entirely blank line; else it deletes the
preceding character, converting tabs to spaces as needed so that only
one character position is deleted.

Variable py-continuation-offset is the additional indentation given to
the first continuation line in a multi-line statement. Each subsequent
continuation line in the statement inherits its indentation from the
line that precedes it, so if you don't like the default indentation
given to the first continuation line, change it to something you do like
and Python-mode will automatically use that for the remaining
continuation lines (or, until you change the indentation again).

Warning: indent-region should not be used! Use indent-rigidly to shift
regions of Python code instead. indent-region can't guess the
indentation you had in mind, and will, e.g., change

xxx
if a < b:
c = d
if e < f: print 'ouch!'

to
xxx
if a < b:
c = d
if e < f: print 'ouch!'

Paragraphs are separated by blank lines only.
\\{py-mode-map}
\\[python-mode] calls the value of the variable py-mode-hook with no args,
if that value is non-nil."
(interactive)
(kill-all-local-variables)
(use-local-map py-mode-map)
(setq major-mode 'python-mode)
(setq mode-name "Python")
(if py-mode-syntax-table
(set-syntax-table py-mode-syntax-table)
(progn
(setq py-mode-syntax-table (make-syntax-table))
(set-syntax-table py-mode-syntax-table)
(modify-syntax-entry ?\( "()")
(modify-syntax-entry ?\) ")(")
(modify-syntax-entry ?\[ "(]")
(modify-syntax-entry ?\] ")[")
(modify-syntax-entry ?\{ "(}")
(modify-syntax-entry ?\} "){")
(modify-syntax-entry ?\_ "w")
(modify-syntax-entry ?\' "\"") ; single quote is string quote
(modify-syntax-entry ?\` "$") ; backquote is open and close paren
(modify-syntax-entry ?\# "<") ; hash starts comment
(modify-syntax-entry ?\n ">") ; newline ends comment
))
(make-local-variable 'paragraph-separate)
(setq paragraph-separate "^[ \t\f]*$")
(make-local-variable 'paragraph-start)
(setq paragraph-start "^[ \t\f]*$")
(make-local-variable 'require-final-newline)
(setq require-final-newline t)
(make-local-variable 'comment-start)
(setq comment-start "# ")
(make-local-variable 'comment-start-skip)
(setq comment-start-skip "# *")
(make-local-variable 'comment-column)
(setq comment-column 40)
(make-local-variable 'indent-line-function)
(setq indent-line-function 'py-indent-line)
(run-hooks 'py-mode-hook))

;; Functions that execute Python commands in a subprocess

(defun py-shell nil
"Start an interactive Python interpreter in another window.
The variable py-python-command names the interpreter."
(interactive)
(require 'shell)
(switch-to-buffer-other-window
(make-shell "Python" py-python-command))
(make-local-variable 'shell-prompt-pattern)
(setq shell-prompt-pattern "^>>> \\|^\\.\\.\\. "))

(defun py-execute-region (start end)
"Send the region between START and END to a Python interpreter.
If there is a *Python* process it is used."
(interactive "r")
(condition-case nil
(process-send-string "Python" (buffer-substring start end))
(error (shell-command-on-region start end py-python-command nil))))

(defun py-execute-buffer nil
"Send the contents of the buffer to a Python interpreter.
If there is a *Python* process buffer it is used."
(interactive)
(py-execute-region (point-min) (point-max)))

;; Functions for Python style indentation

(defun py-delete-char ()
"If at first non-blank character, or at last character of blank line,
reduce indentation by the value of variable py-indent-offset. Else
delete preceding character, converting tabs to spaces."
(interactive)
(backward-delete-char-untabify
(if (and
(= (current-indentation) (current-column))
(>= (current-indentation) py-indent-offset))
py-indent-offset
1)))

(defun py-indent-line ()
"Fix the indentation of the current line according to Python rules."
(interactive)
(let ( (need (py-compute-indentation)) )
(if (= (current-indentation) need)
nil
(beginning-of-line)
(delete-horizontal-space)
(indent-to need))))

; go to start of current statement; usually this is the line we're on,
; but if we're on the 2nd or following lines of a continuation block, we
; need to go up to the first line of the block
(defun py-goto-initial-line ()
(if (re-search-backward "[^\\]\n" (point-min) 1)
(forward-char 2))) ; else leave point at start of restriction

; t iff on continuation line == preceding line ends with backslash
(defun py-continuation-line-p ()
(save-excursion
(beginning-of-line)
(if (< (- (point) 2) (point-min))
nil
(backward-char 2)
(looking-at "\\\\\n"))))

(defun py-compute-indentation ()
(save-excursion
(beginning-of-line)
(cond
;; are we on a continuation line?
( (py-continuation-line-p)
(forward-line -1)
(if (py-continuation-line-p) ; on at least 3rd line in block
(current-indentation) ; so just continue the pattern
;; else on 2nd line in block, so indent more
(+ (current-indentation) py-indent-offset
py-continuation-offset)))
;; not on a continuation line

;; if at start of restriction, assume they intended whatever's there
( (bobp) (current-indentation) )

;; else indentation based on that of the statement that precedes
;; us; use the first line of that statement to establish the base,
;; in case the user forced a non-std indentation for the
;; continuation lines (if any)
( t
(backward-to-indentation 1)
(let ( (need-more-p (looking-at py-colon-line-re)) )
(py-goto-initial-line)
(if need-more-p
(+ (current-indentation) py-indent-offset)
(current-indentation)))))))

;; To do:
;; - add a newline when executing buffer ending in partial line
;; - suppress prompts when executing regions
;; - switch back to previous buffer when starting shell
;; - support for ptags

>>> END OF MSG