Package: haskell-mode Version: 2.3-1 Severity: wishlist Haskell-mode's default list indentation often seems to me do to the wrong thing. Just to take a few examples:
==== module Test(name1, name2, name3) ==== here haskell-mode wants to indent name2 to match the comma after name1. It does produce the "correct" indentation on the second cycle, but there's no reason to produce the incorrect one at all. ==== x = [elt, elt2, elt3, elt4] ==== As before, elt3 would get indented to match the comma after "elt". ==== x = [elt, elt2, ] ==== This one happens because my modus operandi for entering lists is to type "[]", then start filling the list in. haskell-mode indents the closing brace to match the opening brace, rather than placing the cursor where the next list item goes as I would expect. ==== x = [ (n * 2, n) | n <- [1..], ==== Hitting Tab on the empty line here gives me three choices: indent to match [1..], add a second pipe matching the first pipe, or indent to match the open paren of the tuple. None of these indentations make any sense in the current syntactic context (except if I'm using the ghc extension for parallel list comprehensions, but I think haskell-mode is blindly treating the pipe as starting a guard expression). ==== x = [ (n * 2, n) | n <- [1..], n > 50 ] ==== haskell-mode tries to indent "n > 50" to match either "[1..]" or the open paren of the tuple. As before, neither of these makes sense in the current context; the only sensible indentation is the one I've provided above. As far as I can tell from the code, the root cause of these problems is that haskell-mode doesn't have any notion that ending a line with a list separator (that is, a comma or semicolon) changes the indentation context. Commas and semicolons always terminate an expression, so there's no need to scan back for indentation points. I've attached a patch that adds explicit logic for handling the indentation of items in a delimited list. In addition to the cases listed above, this also handles the idiom of placing the list separator at the front of a line as in (1), and one possible idiom for a newline after opening a list as in (2). In the second case, it might be good to provide alternative behavior in the case of a "hanging" open brace, but I'm not quite sure how best to acheive that in the code. (1): x = [ 1 , 2] (2): x = [ 1 ] Note that the patch I've attached also fixes #343248, by not looking past pipe characters when calculating indent levels. Daniel -- System Information: Debian Release: lenny/sid APT prefers unstable APT policy: (500, 'unstable'), (500, 'stable') Architecture: i386 (i686) Kernel: Linux 2.6.22-3-686 (SMP w/1 CPU core) Locale: LANG=en_US.UTF-8, LC_CTYPE=en_US.UTF-8 (charmap=UTF-8) (ignored: LC_ALL set to en_US.UTF-8) Shell: /bin/sh linked to /bin/bash Versions of packages haskell-mode depends on: ii emacs [emacsen] 22.1+1-2.3 The GNU Emacs editor (metapackage) ii emacs21 [emacsen] 21.4a+1-5.3 The GNU Emacs editor ii emacs22-gtk [emacsen] 22.1+1-2.3 The GNU Emacs editor (with GTK use Versions of packages haskell-mode recommends: ii ghc6 6.6.1-2 GHC - the Glasgow Haskell Compilat ii hugs 98.200609.21-5 A Haskell 98 interpreter -- no debconf information
--- haskell-indent.el.orig 2008-01-01 09:16:35.000000000 -0800 +++ haskell-indent.el 2008-01-06 11:59:46.000000000 -0800 @@ -183,6 +183,20 @@ (forward-char 1)) (looking-at "[ \t]*$"))) +(defun haskell-indent-move-to-previous-non-empty-line () + "Like (haskell-indent-forward-line -1) but skips empty lines. +If no non-empty line could be found, point does not move and -1 +is returned; otherwise 0 is returned." + (let* ((start-point (point)) + (rval (forward-line -1))) + (when (zerop rval) + (while (and (haskell-indent-empty-line-p) + (zerop (setq rval (forward-line -1)))) + t)) + (unless (zerop rval) + (goto-char start-point)) + rval)) + (defun haskell-indent-back-to-indentation () "`back-to-indentation' function but dealing with Bird-style literate scripts." (if (and (eq haskell-literate 'bird) @@ -1023,6 +1037,34 @@ (setq open (haskell-indent-virtual-indentation start)))) (list (list (haskell-indent-point-to-col open))))) +(defcustom haskell-indent-after-open-structure + 2 + "Default indentation for the elements of a structure (list, +tuple, etc) relative to the first character of the structure. + +For instance, in the following Haskell code: + +x = [ + \"abra\", + \"cadabra\" + ] + +this controls the indentation of the line containing \"abra\"." + :type 'integer) + +(defcustom haskell-indent-such-that + 0 + "Default indentation for a pipe character within a structure +that begins a line relative to the first character of its +structure. + +For instance, if this is set to 4, it will produce indentation +like the following: + +x = [ (y, y-1) + | y <- [1..]]" + :type 'integer) + (defcustom haskell-indent-after-keywords '(("where" 2 0) ("of" 2) @@ -1097,23 +1139,244 @@ (+ (haskell-indent-virtual-indentation start) (or (cadr offset-info) (car offset-info) default)))) +(defun haskell-indent-find-nearby-list-item-separator () + "Search for a list item separator near point, returning (CHAR . POS) where CHAR is the separator and POS is a symbol indicating where it was found, or nil if none was found. + +If point is at a a list separator (',' or ';'), POS is 'point. +If the point is at the start of a line and the previous line +ended with a list separator or an open brace, POS is 'prev-line. +If not, and if the point is on a close paren/brace or pipe +character, POS is 'point. Otherwise return nil. + +Note that this means that in the following text: + +x = [ a, + b, + ] + +with point on the closing brace, POS will be 'prev-line and not +'point, so hitting Tab will align with \"b\" and not with \"[\". +This is sensible since the user has indicated that she wants to +type another line of code by ending the previous line with a +comma. + +Also note that since pipes are treated like separators here, in +the following text: + +x = [ (a, b) + | (a, b, _) <- triples ] + +hitting Tab on the \"|\" will set POS to 'point." + (save-excursion + (save-match-data + (cond + ;; If we're sitting on a separator, return 'point. + ((looking-at "\\([;,]\\)") + (cons (match-string 1) 'point)) + ;; If the previous line ends with a separator, return + ;; 'prev-line. Skip over empty lines. + ((save-excursion + (and (zerop (haskell-indent-move-to-previous-non-empty-line)) + (let ((start (line-beginning-position))) + (end-of-line) + (skip-chars-backward "[:space:]" start) + (looking-back "[;,]\\|\\s(")))) + (cons (match-string 1) 'prev-line)) + ;; If no separator is nearby but we're sitting on a close paren + ;; or close brace, return 'point. + ((looking-at "\\(\\s)\\||\\)") + (cons (match-string 1) 'point)))))) + +(defun haskell-indent-find-local-indent (&optional pos) + "Find the column of the next non-space character on the current +line. Returns nil if there are no more non-space characters, and +leaves the first character in match-data." + + (save-excursion + (when pos (goto-char pos)) + (assert (looking-at "[ \t]*\\([^ \t]?\\)")) + (if (equal (match-string 1) "\n") + nil + (haskell-indent-point-to-col (match-beginning 1))))) + +(defun haskell-indent-guess-structural-indent-pattern (open) + "Return a pair (SEP . NOSEP) indicating how to indent lines that do and don't begin with an element separator in the structure open at point. +This works by searching backward to the open paren or brace of +the structure, or the first pipe character encountered. + +The most recent indentation of a line starting with a separator +is used for SEP, and the most recent indentation of a line not +starting with a separator, or of the text following a separator, +is used for NOSEP. If NOSEP is determined but SEP cannot be +determined in this manner, then SEP is set to the column of the +boundary or NOSEP - 2, whichever is less." + ;; Note: the reason for determining SEP and NOSEP separately is so + ;; we can cope with lists that place something other than two spaces + ;; between the separator and the list item. Perhaps this is too + ;; much complexity, though? + ;; + ;; So, the algorithm here is to walk backwards with backward-sexp + ;; until either (1) backward-sexp errors out, indicating that we hit + ;; the beginning of the balanced expression, or (2) we find + ;; ourselves looking at a pipe. There's a catch, though: some + ;; versions of haskell-mode consider pipes to be part of a valid + ;; identifier. So in the string + ;; + ;; [a+2|a<-as] + ;; + ;; a backward-sexp will skip over the entire contents of the list. + ;; Fun, no? So instead we have to use this procedure: + ;; (a) scan back to the next closing paren/brace or pipe that's + ;; not commented out. + ;; (a.1) if a pipe is found, indent as if it were an opening brace. + ;; (a.2) if a closing delimiter is found, skip over the balanced + ;; expression. + ;; (a.3) if we hit the beginning of the search region, return. + + (block guess-structural-indent-pattern + ;; Initialize return value counters. + (let* ((sep-indent nil) (no-sep-indent nil) + (minimum (lambda (x y) (if (< x y) x y))) + ;; Remember whether the line being indented starts with a + ;; pipe. + (at-pipe (looking-at "[ \t]*|")) + ;; "finish" is responsible for translating the inferred + ;; indentation levels into a return value by filling in + ;; missing values. open-delim is the position of the + ;; character that starts the list (either an open + ;; paren/brace or a pipe). + (finish (lambda (open-delim) + ;; If no indentation for no-sep lines was found, + ;; default to the indentation (if any) after the + ;; opening brace. + (setq no-sep-indent + (or no-sep-indent (haskell-indent-find-local-indent (+ open-delim 1)))) + + ;; Now fill in missing data. First, if we have + ;; no indentation for lines beginning with + ;; separators, just indent them to match the + ;; opening brace. + ;; + ;; NB: if the line being indented starts with a + ;; pipe character and the opening brace found is + ;; NOT a pipe, maybe apply a little extra + ;; indentation if the user requested that we do + ;; so. + (setq sep-indent + (or sep-indent + (let ((open-col (haskell-indent-point-to-col open-delim))) + (if (and at-pipe + (not (eq ?| (char-after open-delim)))) + (+ haskell-indent-such-that open-col) + open-col)))) + + ;; Now, if we still have no indentation for lines + ;; beginning without a separator, indent them + ;; past lines beginning with a separator as + ;; requested by the user. + (setq no-sep-indent + (or no-sep-indent (+ haskell-indent-after-open-structure sep-indent))) + + (cons sep-indent no-sep-indent)))) + (setq sep-indent nil) + (setq no-sep-indent nil) + (save-excursion + ;; We assume the current line is to be indented, so move to + ;; the last character of the previous line. Otherwise the + ;; current line's present indentation is used to determine its + ;; future indentation (i.e., we do nothing). + (haskell-indent-move-to-previous-non-empty-line) + (end-of-line) + (while (> (point) open) + ;; Find the next pipe, closing brace, element separator or + ;; line break. + (let ((next-bound (looking-back "\\([)}|;\n]\\|\\]\\).*" open))) + (cond + ;; Abort the loop and exit if there is no closing brace. + ((not next-bound) + (return-from guess-structural-indent-pattern (funcall finish open))) + + ;; Skip this match if it's in a comment. + ((haskell-indent-in-comment open (match-beginning 1)) + (goto-char (- (match-beginning 1) 1))) + + ;; If we hit a comma or semicolon at the front of a line, + ;; update the value of sep-indent appropriately. + ;; Otherwise, update nosep-indent. + ((equal "\n" (match-string 1)) + (let ((match-point (match-beginning 1))) + (goto-char (+ 1 match-point)) + (let ((indent-amt (haskell-indent-find-local-indent))) + (if (or (equal (match-string 1) ",") + (equal (match-string 1) ";")) + ;; Compute the indent for lines without + ;; separators from the number of spaces + ;; following the separator. + (let ((new-no-sep-indent + (haskell-indent-find-local-indent (+ 1 (match-beginning 1))))) + (setq sep-indent (or sep-indent indent-amt)) + (setq no-sep-indent (or no-sep-indent new-no-sep-indent))) + (setq no-sep-indent (or no-sep-indent indent-amt))) + (goto-char (- match-point 1))))) + + ;; If it's not in a comment and it's a pipe character, + ;; break out. + ((equal "|" (match-string 1)) + (return-from guess-structural-indent-pattern (funcall finish (match-beginning 1)))) + + ;; If not a pipe, it must be a closing brace or paren; step + ;; over it. If we fail to step over it, just give up and + ;; indent to the open paren. + (t (condition-case errlist + (progn (goto-char (+ 1 (match-beginning 1))) + (backward-sexp)) + (error (return-from guess-structural-indent-pattern (funcall finish open)))))))) + (funcall finish open))))) + +(defun haskell-indent-find-structural-indent (open) + "Find the appropriate indentation for an element of a structure that leads its line. +Point is presumed to be placed at the front of the line +containing the element. + +We determine the indentation by finding the last element that +lead its line and indenting to match it. If there is no suh +element, we indent by 2 spaces relative to the opening character +of the structure. + +If the opening character of the structure is a square +backet ('['), then the most recent pipe character limits where we +will pull indentation from. This means that code like: + +\[ (x, y) | (x, y, z) <- triples, + x < z && y < z \] + +will indent as expected." + (let* ((indent-pattern (haskell-indent-guess-structural-indent-pattern open)) + (sep (haskell-indent-find-nearby-list-item-separator)) + (sep-indent (car indent-pattern)) + (no-sep-indent (cdr indent-pattern))) + (if (not sep) nil + (list (list + (if (haskell-indent-hanging-p) + (haskell-indent-virtual-indentation nil) + (if (eq (cdr sep) 'point) + sep-indent + no-sep-indent))))))) + (defun haskell-indent-inside-paren (open) ;; there is an open structure to complete - (if (looking-at "\\s)\\|[;,]") + (if (setq sep (haskell-indent-find-nearby-list-item-separator)) ;; A close-paren or a , or ; can only correspond syntactically to ;; the open-paren at `open'. So there is no ambiguity. (progn - (if (or (and (eq (char-after) ?\;) (eq (char-after open) ?\()) - (and (eq (char-after) ?\,) (eq (char-after open) ?\{))) - (message "Mismatched punctuation: `%c' in %c...%c" - (char-after) (char-after open) + (if (or (and (equal (car sep) ";") (eq (char-after open) ?\()) + (and (equal (car sep) ",") (eq (char-after open) ?\{)) + (and (equal (car sep) ")") (eq (char-after open) ?\{)) + (and (equal (car sep) "}") (eq (char-after open) ?\())) + (message "Mismatched punctuation: `%s' in %c...%c" + (car sep) (char-after open) (if (eq (char-after open) ?\() ?\) ?\}))) - (save-excursion - (goto-char open) - (list (list - (if (haskell-indent-hanging-p) - (haskell-indent-virtual-indentation nil) - (haskell-indent-point-to-col open)))))) + (haskell-indent-find-structural-indent open)) ;; There might still be layout within the open structure. (let* ((end (point)) (basic-indent-info