1;;; doctest-mode.el --- Major mode for editing Python doctest files
2
3;; Copyright (C) 2004  Edward Loper
4
5;; Author:     Edward Loper
6;; Maintainer: edloper@alum.mit.edu
7;; Created:    Aug 2004
8;; Keywords:   python doctest unittest test docstring
9
10(defconst doctest-version "0.2"
11  "`doctest-mode' version number.")
12
13;; This software is provided as-is, without express or implied
14;; warranty.  Permission to use, copy, modify, distribute or sell this
15;; software, without fee, for any purpose and by any individual or
16;; organization, is hereby granted, provided that the above copyright
17;; notice and this paragraph appear in all copies.
18
19;; This is a major mode for editing text files that contain Python
20;; doctest examples.  Doctest is a testing framework for Python that
21;; emulates an interactive session, and checks the result of each
22;; command.  For more information, see the Python library reference:
23;; <http://docs.python.org/lib/module-doctest.html>
24
25;; Known bugs:
26;; - Some places assume prompts are 4 chars (but they can be 3
27;;   if they're bare).
28;; - String literals are not colored correctly.  (We need to color
29;;   string literals on source lines, but *not* output lines or
30;;   text lines; this is hard to do.)
31;; - Output lines starting with "..." are mistakenly interpreted
32;;   as (continuation) source lines.
33
34;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
35;; Customizable Constants
36;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
37
38(defgroup doctest nil
39  "Support for the Python doctest framework"
40  :group 'languages
41  :prefix "doctest-")
42
43(defcustom doctest-default-margin 4
44  "The default pre-prompt margin for doctest examples."
45  :type 'integer
46  :group 'doctest)
47
48(defcustom doctest-avoid-trailing-whitespace t
49  "If true, then delete trailing whitespace when inserting a newline."
50  :type 'boolean
51  :group 'doctest)
52
53(defcustom doctest-temp-directory
54  (let ((ok '(lambda (x)
55	       (and x
56		    (setq x (expand-file-name x)) ; always true
57		    (file-directory-p x)
58		    (file-writable-p x)
59		    x))))
60    (or (funcall ok (getenv "TMPDIR"))
61	(funcall ok "/usr/tmp")
62	(funcall ok "/tmp")
63	(funcall ok "/var/tmp")
64	(funcall ok  ".")
65	(error (concat "Couldn't find a usable temp directory -- "
66		       "set `doctest-temp-directory'"))))
67
68  "*Directory used for temporary files created when running doctest.
69By default, the first directory from this list that exists and that you
70can write into: the value (if any) of the environment variable TMPDIR,
71/usr/tmp, /tmp, /var/tmp, or the current directory."
72  :type 'string
73  :group 'doctest)
74
75(defcustom hide-example-source t
76  "If true, then don't display the example source code for each
77failure in the results buffer."
78  :type 'boolean
79  :group 'doctest)
80
81(defcustom doctest-python-command "python"
82  "Shell command used to start the python interpreter")
83
84;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
85;; Fonts
86;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
87
88(defface doctest-prompt-face
89  '((((class color) (background dark))
90     (:foreground "#68f"))
91    (t (:foreground "#226")))
92  "Face for Python prompts in doctest examples."
93  :group 'doctest)
94
95(defface doctest-output-face
96  '((((class color) (background dark))
97     (:foreground "#afd"))
98    (t (:foreground "#262")))
99  "Face for the output of doctest examples."
100  :group 'doctest)
101
102(defface doctest-output-marker-face
103  '((((class color) (background dark))
104     (:foreground "#0f0"))
105    (t (:foreground "#080")))
106  "Face for markers in the output of doctest examples."
107  :group 'doctest)
108
109(defface doctest-output-traceback-face
110  '((((class color) (background dark))
111     (:foreground "#f88"))
112    (t (:foreground "#622")))
113  "Face for traceback headers in the output of doctest examples."
114  :group 'doctest)
115
116(defface doctest-results-divider-face
117  '((((class color) (background dark))
118     (:foreground "#08f"))
119    (t (:foreground "#00f")))
120  "Face for dividers in the doctest results window."
121  :group 'doctest)
122
123(defface doctest-results-loc-face
124  '((((class color) (background dark))
125     (:foreground "#0f8"))
126    (t (:foreground "#084")))
127  "Face for location headers in the doctest results window."
128  :group 'doctest)
129
130(defface doctest-results-header-face
131  '((((class color) (background dark))
132     (:foreground "#8ff"))
133    (t (:foreground "#088")))
134  "Face for sub-headers in the doctest results window."
135  :group 'doctest)
136
137(defface doctest-results-selection-face
138  '((((class color) (background dark))
139     (:foreground "#ff0" :background "#008"))
140    (t (:background "#088" :foreground "#fff")))
141  "Face for selected failure's location header in the results window."
142  :group 'doctest)
143
144(defface doctest-selection-face
145  '((((class color) (background dark))
146     (:foreground "#ff0" :background "#00f" :bold t))
147    (t (:foreground "#f00")))
148  "Face for selected example's prompt"
149  :group 'doctest)
150
151;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
152;; Constants
153;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
154
155(defconst doctest-prompt-re
156  "^\\([ \t]*\\)\\(>>> ?\\|[.][.][.] ?\\)\\([ \t]*\\)"
157  "Regular expression for doctest prompts.  It defines three groups:
158the pre-prompt margin; the prompt; and the post-prompt indentation.")
159
160(defconst doctest-open-block-re
161  "[^\n]+:[ \t]*\\(#.*\\)?$"
162  "Regular expression for a line that opens a block")
163
164(defconst doctest-close-block-re
165  "\\(return\\|raise\\|break\\|continue\\|pass\\)\\b"
166  "Regular expression for a line that closes a block")
167
168(defconst doctest-outdent-re
169  (concat "\\(" (mapconcat 'identity
170			   '("else:"
171			     "except\\(\\s +.*\\)?:"
172			     "finally:"
173			     "elif\\s +.*:")
174			   "\\|")
175	  "\\)")
176  "Regular expression for a line that should be outdented.  Any line
177that matches `doctest-outdent-re', but does not follow a line matching
178`doctest-no-outdent-re', will be outdented.")
179
180(defconst doctest-no-outdent-re
181  (concat
182   "\\("
183   (mapconcat 'identity
184	      (list "try:"
185		    "except\\(\\s +.*\\)?:"
186		    "while\\s +.*:"
187		    "for\\s +.*:"
188		    "if\\s +.*:"
189		    "elif\\s +.*:"
190                    "\\(return\\|raise\\|break\\|continue\\|pass\\)[ \t\n]"
191		    )
192	      "\\|")
193	  "\\)")
194  "Regular expression matching lines not to outdent after.  Any line
195that matches `doctest-outdent-re', but does not follow a line matching
196`doctest-no-outdent-re', will be outdented.")
197
198;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
199;; Colorization support (font-lock mode)
200;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
201
202;; Define the font-lock keyword table.
203(defconst doctest-font-lock-keywords
204  (let ((prompt "^[ \t]*\\(>>>\\|\\.\\.\\.\\)")
205        (kw1 (mapconcat 'identity
206			'("and"      "assert"   "break"   "class"
207			  "continue" "def"      "del"     "elif"
208			  "else"     "except"   "exec"    "for"
209			  "from"     "global"   "if"      "import"
210			  "in"       "is"       "lambda"  "not"
211			  "or"       "pass"     "print"   "raise"
212			  "return"   "while"    "yield"
213			  )
214			"\\|"))
215	(kw2 (mapconcat 'identity
216			'("else:" "except:" "finally:" "try:")
217			"\\|"))
218	(kw3 (mapconcat 'identity
219			'("ArithmeticError" "AssertionError"
220			  "AttributeError" "DeprecationWarning" "EOFError"
221			  "Ellipsis" "EnvironmentError" "Exception" "False"
222			  "FloatingPointError" "FutureWarning" "IOError"
223			  "ImportError" "IndentationError" "IndexError"
224			  "KeyError" "KeyboardInterrupt" "LookupError"
225			  "MemoryError" "NameError" "None" "NotImplemented"
226			  "NotImplementedError" "OSError" "OverflowError"
227			  "OverflowWarning" "PendingDeprecationWarning"
228			  "ReferenceError" "RuntimeError" "RuntimeWarning"
229			  "StandardError" "StopIteration" "SyntaxError"
230			  "SyntaxWarning" "SystemError" "SystemExit"
231			  "TabError" "True" "TypeError" "UnboundLocalError"
232			  "UnicodeDecodeError" "UnicodeEncodeError"
233			  "UnicodeError" "UnicodeTranslateError"
234			  "UserWarning" "ValueError" "Warning"
235			  "ZeroDivisionError" "__debug__"
236			  "__import__" "__name__" "abs" "apply" "basestring"
237			  "bool" "buffer" "callable" "chr" "classmethod"
238			  "cmp" "coerce" "compile" "complex" "copyright"
239			  "delattr" "dict" "dir" "divmod"
240			  "enumerate" "eval" "execfile" "exit" "file"
241			  "filter" "float" "getattr" "globals" "hasattr"
242			  "hash" "hex" "id" "input" "int" "intern"
243			  "isinstance" "issubclass" "iter" "len" "license"
244			  "list" "locals" "long" "map" "max" "min" "object"
245			  "oct" "open" "ord" "pow" "property" "range"
246			  "raw_input" "reduce" "reload" "repr" "round"
247			  "setattr" "slice" "staticmethod" "str" "sum"
248			  "super" "tuple" "type" "unichr" "unicode" "vars"
249			  "xrange" "zip")
250			"\\|"))
251        (pseudokw (mapconcat 'identity
252                        '("self" "None" "True" "False" "Ellipsis")
253                        "\\|"))
254        (brk "\\([ \t(]\\|$\\)")
255	)
256    `(
257      ;; The following pattern colorizes source lines.  In particular,
258      ;; it first matches prompts, and then looks for any of the
259      ;; following matches *on the same line* as the prompt.  It uses
260      ;; the form:
261      ;;
262      ;;   (MATCHER MATCH-HIGHLIGHT
263      ;;            (ANCHOR-MATCHER nil nil MATCH-HIGHLIGHT)
264      ;;            ...
265      ;;            (ANCHOR-MATCHER nil nil MATCH-HIGHLIGHT))
266      ;;
267      ;; See the variable documentation for font-lock-keywords for a
268      ;; description of what each of those means.
269      (,prompt (1 'doctest-prompt-face)
270               ;; classes
271               ("\\b\\(class\\)[ \t]+\\([a-zA-Z_]+[a-zA-Z0-9_]*\\)"
272                nil nil (1 'font-lock-keyword-face)
273                (2 'font-lock-type-face))
274               ;; functions
275               ("\\b\\(def\\)[ \t]+\\([a-zA-Z_]+[a-zA-Z0-9_]*\\)"
276                nil nil (1 'font-lock-keyword-face) (2 'font-lock-type-face))
277               ;; keywords
278               (,(concat "\\b\\(" kw1 "\\)" brk)
279                nil nil (1 'font-lock-keyword-face))
280               ;; builtins when they don't appear as object attributes
281               (,(concat "\\(\\b\\|[.]\\)\\(" kw3 "\\)" brk)
282                nil nil (2 'font-lock-keyword-face))
283               ;; block introducing keywords with immediately
284               ;; following colons.  Yes "except" is in both lists.
285               (,(concat "\\b\\(" kw2 "\\)" brk)
286                nil nil (1 'font-lock-keyword-face))
287               ;; `as' but only in "import foo as bar"
288               ("[ \t]*\\(\\bfrom\\b.*\\)?\\bimport\\b.*\\b\\(as\\)\\b"
289                nil nil (2 'font-lock-keyword-face))
290               ;; pseudo-keywords
291               (,(concat "\\b\\(" pseudokw "\\)" brk)
292                nil nil (1 'font-lock-keyword-face))
293               ;; comments
294               ("\\(#.*\\)"
295                nil nil (1 'font-lock-comment-face)))
296
297      ;; The following pattern colorizes output lines.  In particular,
298      ;; it uses doctest-output-line-matcher to check if this is an
299      ;; output line, and if so, it colorizes it, and any special
300      ;; markers it contains.
301      (doctest-output-line-matcher
302       (0 'doctest-output-face t)
303       ("\\.\\.\\." (beginning-of-line) (end-of-line)
304	(0 'doctest-output-marker-face t))
305       ("<BLANKLINE>" (beginning-of-line) (end-of-line)
306	(0 'doctest-output-marker-face t))
307       ("^Traceback (most recent call last):" (beginning-of-line) (end-of-line)
308	(0 'doctest-output-traceback-face t))
309       ("^Traceback (innermost last):" (beginning-of-line) (end-of-line)
310	(0 'doctest-output-traceback-face t))
311       )
312
313      ;; A PS1 prompt followed by a non-space is an error.
314      ("^[ \t]*\\(>>>[^ \t\n][^\n]*\\)" (1 'font-lock-warning-face t))
315
316      ;; Selected example (to highlight selected failure)
317      (doctest-selection-matcher (0 'doctest-selection-face t))
318      ))
319  "Expressions to highlight in Doctest mode.")
320
321(defun doctest-output-line-matcher (limit)
322  "A `font-lock-keyword' MATCHER that returns t if the current
323line is the expected output for a doctest example, and if so,
324sets `match-data' so that group 0 spans the current line."
325  ;; The real work is done by find-doctest-output-line.
326  (when (find-doctest-output-line limit)
327    ;; If we found one, then mark the entire line.
328    (beginning-of-line)
329    (search-forward-regexp "[^\n]*" limit)))
330
331;; [XX] Under construction.
332(defun doctest-selection-matcher (limit)
333  (let (found-it)
334    (while (and (not found-it)
335                (search-forward-regexp "^[ \t]*\\(>>>\\|[.][.][.]\\)"
336                                       limit t))
337      (if (get-text-property (point) 'doctest-selected)
338          (setq found-it t)))
339    found-it))
340
341;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
342;; Source line indentation
343;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
344
345(defun doctest-indent-source-line (&optional dedent-only)
346  "Re-indent the current line, as doctest source code.  I.e., add a
347prompt to the current line if it doesn't have one, and re-indent the
348source code (to the right of the prompt).  If `dedent-only' is true,
349then don't increase the indentation level any."
350  (interactive "*")
351  (let ((indent-end nil))
352    (save-excursion
353      (beginning-of-line)
354      (let ((new-indent (doctest-current-source-line-indentation dedent-only))
355            (new-margin (doctest-current-source-line-margin))
356            (line-had-prompt (looking-at doctest-prompt-re)))
357        ;; Delete the old prompt (if any).
358        (when line-had-prompt
359          (goto-char (match-end 1))
360          (delete-char 4))
361        ;; Delete the old indentation.
362        (delete-backward-char (skip-chars-forward " \t"))
363        ;; If it's a continuation line, or a new PS1 prompt,
364        ;; then copy the margin.
365        (when (or new-indent (not line-had-prompt))
366          (beginning-of-line)
367          (delete-backward-char (skip-chars-forward " \t"))
368          (insert-char ?\  new-margin))
369        ;; Add the new prompt.
370        (insert-string (if new-indent "... " ">>> "))
371        ;; Add the new indentation
372        (if new-indent (insert-char ?\  new-indent))
373        (setq indent-end (point))))
374    ;; If we're left of the indentation end, then move up to the
375    ;; indentation end.
376    (if (< (point) indent-end) (goto-char indent-end))))
377
378(defun doctest-current-source-line-indentation (&optional dedent-only)
379  "Return the post-prompt indent to use for this line.  This is an
380integer for a continuation lines, and nil for non-continuation lines."
381  (save-excursion
382    (let ((prev-line-indent 0)
383          (curr-line-indent 0)
384          (prev-line-opens-block nil)
385          (prev-line-closes-block nil)
386          (curr-line-outdented nil))
387      ;; Examine this doctest line.
388      (beginning-of-line)
389      (when (looking-at doctest-prompt-re)
390          (setq curr-line-indent (- (match-end 3) (match-beginning 3)))
391	  (goto-char (match-end 3)))
392      (setq curr-line-outdented (looking-at doctest-outdent-re))
393      ;; Examine the previous line.
394      (when (= (forward-line -1) 0) ; move up a line
395	(when (looking-at doctest-prompt-re) ; is it a source line?
396	  (let ((indent-beg (column-at-char (match-beginning 3)))
397		(indent-end (column-at-char (match-end 3))))
398	    (setq prev-line-indent (- indent-end indent-beg))
399	    (goto-char (match-end 3))
400	    (if (looking-at doctest-open-block-re)
401		(setq prev-line-opens-block t))
402	    (if (looking-at doctest-close-block-re)
403		(setq prev-line-closes-block t))
404	    (if (looking-at doctest-no-outdent-re)
405		(setq curr-line-outdented nil))
406	    )))
407      (let ((indent (+ prev-line-indent
408                       (if curr-line-outdented -4 0)
409                       (if prev-line-opens-block 4 0)
410                       (if prev-line-closes-block -4 0))))
411	;; If dedent-only is true, then make sure we don't indent.
412	(when dedent-only
413	  (setq indent (min indent curr-line-indent)))
414	;; If indent=0 and we're not outdented, then set indent to
415	;; nil (to signify the start of a new source example).
416	(when (and (= indent 0) (not curr-line-outdented))
417	  (setq indent nil))
418	;; Return the indentation.
419	indent))))
420
421(defun doctest-current-source-line-margin ()
422  "Return the pre-prompt margin to use for this source line.  This is
423copied from the most recent source line, or set to
424`doctest-default-margin' if there are no preceeding source lines."
425  (save-excursion
426    (beginning-of-line)
427    (if (search-backward-regexp doctest-prompt-re nil t)
428        (let ((margin-beg (column-at-char (match-beginning 1)))
429              (margin-end (column-at-char (match-end 1))))
430          (- margin-end margin-beg))
431      doctest-default-margin)))
432
433(defun doctest-electric-backspace ()
434  "Delete the preceeding character, level of indentation, or
435prompt.
436
437If point is at the leftmost column, delete the preceding newline.
438
439Otherwise, if point is at the first non-whitespace character
440following an indented source line's prompt, then reduce the
441indentation to the next multiple of 4; and update the source line's
442prompt, when necessary.
443
444Otherwise, if point is at the first non-whitespace character
445following an unindented source line's prompt, then remove the
446prompt (converting the line to an output line or text line).
447
448Otherwise, if point is at the first non-whitespace character of a
449line, the delete the line's indentation.
450
451Otherwise, delete the preceeding character.
452"
453  (interactive "*")
454  (cond
455   ;; Beginning of line: delete preceeding newline.
456   ((bolp) (backward-delete-char 1))
457
458   ;; First non-ws char following prompt: dedent or remove prompt.
459   ((and (looking-at "[^ \t\n]\\|$") (doctest-looking-back doctest-prompt-re))
460    (let* ((prompt-beg (match-beginning 2))
461	   (indent-beg (match-beginning 3)) (indent-end (match-end 3))
462	   (old-indent (- indent-end indent-beg))
463	   (new-indent (* (/ (- old-indent 1) 4) 4)))
464      (cond
465       ;; Indented source line: dedent it.
466       ((> old-indent 0)
467	(goto-char indent-beg)
468	(delete-region indent-beg indent-end)
469	(insert-char ?\  new-indent)
470	;; Change prompt to PS1, when appropriate.
471	(when (and (= new-indent 0) (not (looking-at doctest-outdent-re)))
472	  (delete-backward-char 4)
473	  (insert-string ">>> ")))
474       ;; Non-indented source line: remove prompt.
475       (t
476	(goto-char indent-end)
477	(delete-region prompt-beg indent-end)))))
478
479   ;; First non-ws char of a line: delete all indentation.
480   ((and (looking-at "[^ \n\t]\\|$") (doctest-looking-back "^[ \t]+"))
481    (delete-region (match-beginning 0) (match-end 0)))
482
483   ;; Otherwise: delete a character.
484   (t
485    (backward-delete-char 1))))
486
487(defun doctest-newline-and-indent ()
488  "Insert a newline, and indent the new line appropriately.
489
490If the current line is a source line containing a bare prompt,
491then clear the current line, and insert a newline.
492
493Otherwise, if the current line is a source line, then insert a
494newline, and add an appropriately indented prompt to the new
495line.
496
497Otherwise, if the current line is an output line, then insert a
498newline and indent the new line to match the example's margin.
499
500Otherwise, insert a newline.
501
502If `doctest-avoid-trailing-whitespace' is true, then clear any
503whitespace to the left of the point before inserting a newline.
504"
505  (interactive "*")
506  ;; If we're avoiding trailing spaces, then delete WS before point.
507  (if doctest-avoid-trailing-whitespace
508      (delete-char (- (skip-chars-backward " \t"))))
509  (cond
510   ;; If we're on an empty prompt, delete it.
511   ((on-empty-doctest-source-line)
512    (delete-region (match-beginning 0) (match-end 0))
513    (insert-char ?\n 1))
514   ;; If we're on a doctest line, add a new prompt.
515   ((on-doctest-source-line)
516    (insert-char ?\n 1)
517    (doctest-indent-source-line))
518   ;; If we're in doctest output, indent to the margin.
519   ((on-doctest-output-line)
520    (insert-char ?\n 1)
521    (insert-char ?\  (doctest-current-source-line-margin)))
522   ;; Otherwise, just add a newline.
523   (t (insert-char ?\n 1))))
524
525(defun doctest-electric-colon ()
526  "Insert a colon, and dedent the line when appropriate."
527  (interactive "*")
528  (insert-char ?: 1)
529  (when (on-doctest-source-line)
530    (doctest-indent-source-line t)))
531
532;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
533;; Code Execution
534;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
535
536;; Add support for options (eg diff!)
537(defun doctest-execute-buffer ()
538  "Run doctest on the current buffer, and display the results in the
539*doctest-output* buffer."
540  (interactive "*")
541  (setq doctest-results-buffer (get-buffer-create "*doctest-output*"))
542  (let* ((temp (concat (doctest-temp-name) ".py"))
543	 (tempfile (expand-file-name temp doctest-temp-directory))
544	 (cur-buf (current-buffer))
545	 (in-buf (get-buffer-create "*doctest-input*"))
546	 (beg (point-min)) (end (point-max))
547	 (script (concat "from doctest import *\n"
548			 "doc = open('" tempfile "').read()\n"
549			 "test = DocTestParser().get_doctest("
550			         "doc, {}, '" (buffer-name) "', '"
551				 (buffer-file-name) "', 0)\n"
552			 "r = DocTestRunner()\n"
553			 "r.run(test)\n"))
554	 (cmd (concat doctest-python-command " -c \"" script "\"")))
555    ;; Write buffer to a file.
556    (save-excursion
557      (set-buffer in-buf)
558      (insert-buffer-substring cur-buf beg end)
559      (write-file tempfile))
560    ;; Run doctest
561    (shell-command cmd doctest-results-buffer)
562    ;; Delete the temp file
563    (delete-file tempfile)
564    ;; Set mode on output buffer.
565    (save-excursion
566      (set-buffer doctest-results-buffer)
567      (doctest-results-mode))
568    ;; If any tests failed, display them.
569    (cond ((> (buffer-size doctest-results-buffer) 0)
570	   (message "Test failed!")
571	   (display-buffer doctest-results-buffer)
572	   (doctest-postprocess-results))
573	  (t
574	   (message "Test passed!")
575	   (if (get-buffer-window doctest-results-buffer)
576	       (delete-window (get-buffer-window doctest-results-buffer)))))))
577
578(defun doctest-postprocess-results ()
579  (doctest-next-failure 1)
580  (if hide-example-source
581    (hide-example-source)))
582
583(defun doctest-next-failure (count)
584  "Move to the top of the next failing example, and highlight the
585example's failure description in *doctest-output*."
586  (interactive "p")
587  (let (lineno)
588    (cond
589     ((not (buffer-live-p doctest-results-buffer))
590      (message "Run doctest first! (C-c C-c)"))
591     (t
592      (save-excursion
593        (let ((orig-window (selected-window))
594              (results-window (display-buffer doctest-results-buffer)))
595          ;; Switch to the results window (so its point gets updated)
596          (if results-window (select-window results-window))
597          ;; Pick up where we left off.
598          ;; (nb: doctest-selected-failure is buffer-local)
599          (goto-char (or doctest-selected-failure (point-min)))
600          ;; Skip past anything on *this* line.
601          (if (>= count 0) (end-of-line) (beginning-of-line))
602          ;; Look for the next failure
603          (if (>= count 0)
604              (re-search-forward doctest-results-loc-re nil t count)
605            (re-search-backward doctest-results-loc-re nil t (- count)))
606          (cond
607           ;; We found a failure:
608           ((match-string 2)
609            (let ((old-selected-failure doctest-selected-failure))
610              ;; Extract the line number for the doctest file.
611              (setq lineno (string-to-int (match-string 2)))
612              ;; Store our position for next time.
613              (beginning-of-line)
614              (setq doctest-selected-failure (point))
615              ;; Update selection.
616              (doctest-fontify-line old-selected-failure)
617              (doctest-fontify-line doctest-selected-failure)))
618           ;; We didn't find a failure:
619           (t
620            (message "No failures found!")))
621          ;; Return to the original window
622          (select-window orig-window)))))
623
624    (when lineno
625      ;; Move point to the selected failure.
626      (goto-line lineno)
627;      ;; Highlight it. [XX] Under construction.
628;      (let ((beg (save-excursion (beginning-of-line) (point)))
629;            (end (save-excursion (end-of-line) (point))))
630;        (add-text-properties (point-min) (point-max) '(doctest-selected nil))
631;        (add-text-properties beg end '(doctest-selected t))
632;        (doctest-fontify-line (point)))
633      )))
634
635(defun doctest-prev-failure (count)
636  "Move to the top of the previous failing example, and highlight
637the example's failure description in *doctest-output*."
638  (interactive "p")
639  (doctest-next-failure (- count)))
640
641(defun doctest-first-failure ()
642  (interactive "")
643  (if (buffer-live-p doctest-results-buffer)
644      (save-excursion
645        (set-buffer doctest-results-buffer)
646        (let ((old-selected-failure doctest-selected-failure))
647          (setq doctest-selected-failure (point-min))
648          (doctest-fontify-line old-selected-failure))))
649  (doctest-next-failure 1))
650
651(defun doctest-last-failure ()
652  (interactive "")
653  (if (buffer-live-p doctest-results-buffer)
654      (save-excursion
655        (set-buffer doctest-results-buffer)
656        (let ((old-selected-failure doctest-selected-failure))
657          (setq doctest-selected-failure (point-max))
658          (doctest-fontify-line old-selected-failure))))
659  (doctest-next-failure -1))
660
661(defconst doctest-example-source-re
662  "^Failed example:\n\\(\n\\|    [^\n]*\n\\)+")
663(defun hide-example-source ()
664  "Delete the source code listings from the results buffer (since it's
665easy enough to see them in the original buffer)"
666  (save-excursion
667    (set-buffer doctest-results-buffer)
668    (toggle-read-only nil)
669    (beginning-of-buffer)
670    (while (re-search-forward doctest-example-source-re nil t)
671      (replace-match "" nil nil))
672    (toggle-read-only t)))
673
674;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
675;; Doctest Results Mode (output of doctest-execute-buffer)
676;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
677;; [XX] Todo:
678;;   - Make it read-only?
679;;   - Hitting enter goes to the corresponding error
680;;   - Clicking goes to corresponding error (not as useful)
681
682
683(defconst doctest-results-divider-re
684  "^\\([*]\\{60,\\}\\)$")
685
686(defconst doctest-results-loc-re
687  "^File \"\\([^\"]+\\)\", line \\([0-9]+\\), in \\([^\n]+\\)")
688
689(defconst doctest-results-header-re
690  "^\\([a-zA-Z0-9 ]+:\\)$")
691
692(defconst doctest-results-font-lock-keywords
693  `((,doctest-results-divider-re
694     (0 'doctest-results-divider-face))
695    (,doctest-results-loc-re
696     (0 'doctest-results-loc-face))
697    (,doctest-results-header-re
698     (0 'doctest-results-header-face))
699    (doctest-results-selection-matcher
700     (0 'doctest-results-selection-face t))))
701
702(defun doctest-results-selection-matcher (limit)
703  "Matches from `doctest-selected-failure' to the end of the
704line.  This is used to highlight the currently selected failure."
705  (when (and doctest-selected-failure
706	     (<= (point) doctest-selected-failure)
707	     (< doctest-selected-failure limit))
708    (goto-char doctest-selected-failure)
709    (search-forward-regexp "[^\n]+" limit)))
710
711;; Register the font-lock keywords (xemacs)
712(put 'doctest-results-mode 'font-lock-defaults
713     '(doctest-results-font-lock-keywords))
714
715;; Register the font-lock keywords (gnu emacs)
716(defvar font-lock-defaults-alist nil) ; in case we're in xemacs
717(setq font-lock-defaults-alist
718      (append font-lock-defaults-alist
719              `((doctest-results-mode
720		 doctest-results-font-lock-keywords
721		 nil nil nil nil))))
722
723;; Define the mode
724(define-derived-mode doctest-results-mode text-mode "Doctest Results"
725  "docstring"
726  ;; Enable font-lock mode.
727  (if (featurep 'font-lock) (font-lock-mode 1))
728  ;; Keep track of which failure is selected
729  (set (make-local-variable 'doctest-selected-failure) nil)
730  ;; Make the buffer read-only.
731  (toggle-read-only t))
732
733;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
734;; Helper functions
735;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
736
737(defun on-doctest-source-line ()
738  "Return true if the current line is a source line."
739  (save-excursion
740    (beginning-of-line)
741    (looking-at doctest-prompt-re)))
742
743(defun on-empty-doctest-source-line ()
744  "Return true if the current line contains a bare prompt."
745  (save-excursion
746    (beginning-of-line)
747    (looking-at (concat doctest-prompt-re "$"))))
748
749(defun on-doctest-output-line ()
750  "Return true if the current line is an output line."
751  (save-excursion
752    (beginning-of-line)
753    (let ((prompt-or-blankline (concat doctest-prompt-re "\\|" "^[ \t]*\n")))
754      ;; The line must not be blank or start with a prompt.
755      (when (not (looking-at prompt-or-blankline))
756          ;; The line must follow a line starting with a prompt, with
757          ;; no intervening blank lines.
758          (search-backward-regexp prompt-or-blankline nil t)
759          (looking-at doctest-prompt-re)))))
760
761(defun find-doctest-output-line (&optional limit)
762  "Move forward to the next doctest output line (staying within
763the given bounds).  Return the character position of the doctest
764output line if one was found, and false otherwise."
765  (let ((found-it nil) ; point where we found an output line
766	(limit (or limit (point-max)))) ; default value for limit
767    (save-excursion
768      ;; Keep moving forward, one line at a time, until we find a
769      ;; doctest output line.
770      (while (and (not found-it) (< (point) limit) (not (eobp)))
771	(if (and (not (eolp)) (on-doctest-output-line))
772	    (setq found-it (point))
773	  (forward-line))))
774    ;; If we found a doctest output line, then go to it.
775    (if found-it (goto-char found-it))))
776
777(defun doctest-version ()
778  "Echo the current version of `doctest-mode' in the minibuffer."
779  (interactive)
780  (message "Using `doctest-mode' version %s" doctest-version))
781
782;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
783;; Utility functions
784;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
785
786(defvar doctest-serial-number 0) ;used if broken-temp-names.
787(defun doctest-temp-name ()
788  (if (memq 'broken-temp-names features)
789      (let
790	  ((sn doctest-serial-number)
791	   (pid (and (fboundp 'emacs-pid) (emacs-pid))))
792	(setq doctest-serial-number (1+ doctest-serial-number))
793	(if pid
794	    (format "doctest-%d-%d" sn pid)
795	  (format "doctest-%d" sn)))
796    (make-temp-name "doctest-")))
797
798(defun column-at-char (pos)
799  "Return the column of the given character position"
800  (save-excursion (goto-char pos) (current-column)))
801
802(defun doctest-looking-back (regexp)
803  "Return True if the text before point matches the given regular
804expression.  Like looking-at except backwards and slower.  (This
805is available as `looking-back' in GNU emacs and
806`looking-at-backwards' in XEmacs, but it's easy enough to define
807from scratch such that it works under both.)"
808  (save-excursion
809    (let ((orig-pos (point)))
810      ;; Search backwards for the regexp.
811      (if (re-search-backward regexp nil t)
812	  ;; Check if it ends at the original point.
813	  (= orig-pos (match-end 0))))))
814
815(defun doctest-fontify-line (charpos)
816  "Run font-lock-fontify-region on the line containing the given
817position."
818  (if charpos
819      (save-excursion
820        (goto-char charpos)
821        (let ((beg (progn (beginning-of-line) (point)))
822              (end (progn (end-of-line) (point))))
823          (font-lock-fontify-region beg end)))))
824
825;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
826;; Syntax Table
827;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
828
829;; We do *NOT* currently use this, because it applies too
830;; indiscrimanantly.  In particular, we don't want "'" and '"' treated
831;; as quote marks on text lines.  But there's no good way to prevent
832;; it.
833(defvar doctest-syntax-alist nil
834  "Syntax alist used in `doctest-mode' buffers.")
835(setq doctest-syntax-alist '((?\( . "()") (?\[ . "(]") (?\{ . "(}")
836			     (?\) . ")(") (?\] . ")[") (?\} . "){")
837			     (?\$ . "." ) (?\% . "." ) (?\& . "." )
838			     (?\* . "." ) (?\+ . "." ) (?\- . "." )
839			     (?\/ . "." ) (?\< . "." ) (?\= . "." )
840			     (?\> . "." ) (?\| . "." ) (?\_ . "w" )
841			     (?\' . "\"") (?\" . "\"") (?\` . "$" )
842			     (?\# . "<" ) (?\n . ">" )))
843
844;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
845;; Key Bindings
846;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
847
848(defconst doctest-mode-map
849  (let ((map (make-keymap)))
850    (define-key map [backspace] 'doctest-electric-backspace)
851    (define-key map [return] 'doctest-newline-and-indent)
852    (define-key map [tab] 'doctest-indent-source-line)
853    (define-key map ":" 'doctest-electric-colon)
854    (define-key map "\C-c\C-v" 'doctest-version)
855    (define-key map "\C-c\C-c" 'doctest-execute-buffer)
856    (define-key map "\C-c\C-n" 'doctest-next-failure)
857    (define-key map "\C-c\C-p" 'doctest-prev-failure)
858    (define-key map "\C-c\C-a" 'doctest-first-failure)
859    (define-key map "\C-c\C-z" 'doctest-last-failure)
860    map)
861  "Keymap for doctest-mode.")
862
863;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
864;; Define the mode
865;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
866
867;; Register the font-lock keywords (xemacs)
868(put 'doctest-mode 'font-lock-defaults '(doctest-font-lock-keywords))
869
870;; Register the font-lock keywords (gnu emacs)
871(defvar font-lock-defaults-alist nil) ; in case we're in xemacs
872(setq font-lock-defaults-alist
873      (append font-lock-defaults-alist
874              `((doctest-mode doctest-font-lock-keywords nil nil nil nil))))
875
876;; Use doctest mode for files ending in .doctest
877;;;###autoload
878(add-to-list 'auto-mode-alist '("\\.doctest$" . doctest-mode))
879
880;;;###autoload
881(define-derived-mode doctest-mode text-mode "Doctest"
882  "A major mode for editing text files that contain Python
883doctest examples.  Doctest is a testing framework for Python that
884emulates an interactive session, and checks the result of each
885command.  For more information, see the Python library reference:
886<http://docs.python.org/lib/module-doctest.html>
887
888`doctest-mode' defines three kinds of line, each of which is
889treated differently:
890
891  - 'Source lines' are lines consisting of a Python prompt
892    ('>>>' or '...'), followed by source code.  Source lines are
893    colored (similarly to `python-mode') and auto-indented.
894
895  - 'Output lines' are non-blank lines immediately following
896    source lines.  They are colored using several doctest-
897    specific output faces.
898
899  - 'Text lines' are any other lines.  They are not processed in
900    any special way.
901
902\\{doctest-mode-map}
903"
904  ;; Enable auto-fill mode.
905  (auto-fill-mode 1)
906
907  ;; Enable font-lock mode.
908  (if (featurep 'font-lock) (font-lock-mode 1))
909
910  ;; Register our indentation function.
911  (set (make-local-variable 'indent-line-function)
912       'doctest-indent-source-line)
913
914  ;; Keep track of our results buffer.
915  (set (make-local-variable 'doctest-results-buffer) nil)
916  )
917
918(provide 'doctest-mode)
919;;; doctest-mode.el ends here
920