summaryrefslogtreecommitdiff
path: root/lisp/emacs-lisp/smie.el
diff options
context:
space:
mode:
Diffstat (limited to 'lisp/emacs-lisp/smie.el')
-rw-r--r--lisp/emacs-lisp/smie.el877
1 files changed, 877 insertions, 0 deletions
diff --git a/lisp/emacs-lisp/smie.el b/lisp/emacs-lisp/smie.el
new file mode 100644
index 00000000000..fb1e4737d39
--- /dev/null
+++ b/lisp/emacs-lisp/smie.el
@@ -0,0 +1,877 @@
+;;; smie.el --- Simple Minded Indentation Engine
+
+;; Copyright (C) 2010 Free Software Foundation, Inc.
+
+;; Author: Stefan Monnier <monnier@iro.umontreal.ca>
+;; Keywords: languages, lisp, internal, parsing, indentation
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; While working on the SML indentation code, the idea grew that maybe
+;; I could write something generic to do the same thing, and at the
+;; end of working on the SML code, I had a pretty good idea of what it
+;; could look like. That idea grew stronger after working on
+;; LaTeX indentation.
+;;
+;; So at some point I decided to try it out, by writing a new
+;; indentation code for Coq while trying to keep most of the code
+;; "table driven", where only the tables are Coq-specific. The result
+;; (which was used for Beluga-mode as well) turned out to be based on
+;; something pretty close to an operator precedence parser.
+
+;; So here is another rewrite, this time following the actual principles of
+;; operator precedence grammars. Why OPG? Even though they're among the
+;; weakest kinds of parsers, these parsers have some very desirable properties
+;; for Emacs:
+;; - most importantly for indentation, they work equally well in either
+;; direction, so you can use them to parse backward from the indentation
+;; point to learn the syntactic context;
+;; - they work locally, so there's no need to keep a cache of
+;; the parser's state;
+;; - because of that locality, indentation also works just fine when earlier
+;; parts of the buffer are syntactically incorrect since the indentation
+;; looks at "as little as possible" of the buffer to make an indentation
+;; decision.
+;; - they typically have no error handling and can't even detect a parsing
+;; error, so we don't have to worry about what to do in case of a syntax
+;; error because the parser just automatically does something. Better yet,
+;; we can afford to use a sloppy grammar.
+
+;; The development (especially the parts building the 2D precedence
+;; tables and then computing the precedence levels from it) is largely
+;; inspired from page 187-194 of "Parsing techniques" by Dick Grune
+;; and Ceriel Jacobs (BookBody.pdf available at
+;; http://www.cs.vu.nl/~dick/PTAPG.html).
+;;
+;; OTOH we had to kill many chickens, read many coffee grounds, and practice
+;; untold numbers of black magic spells, to come up with the indentation code.
+;; Since then, some of that code has been beaten into submission, but the
+;; smie-indent-keyword is still pretty obscure.
+
+;;; Code:
+
+(eval-when-compile (require 'cl))
+
+(defvar comment-continue)
+(declare-function comment-string-strip "newcomment" (str beforep afterp))
+
+;;; Building precedence level tables from BNF specs.
+
+(defun smie-set-prec2tab (table x y val &optional override)
+ (assert (and x y))
+ (let* ((key (cons x y))
+ (old (gethash key table)))
+ (if (and old (not (eq old val)))
+ (if (and override (gethash key override))
+ ;; FIXME: The override is meant to resolve ambiguities,
+ ;; but it also hides real conflicts. It would be great to
+ ;; be able to distinguish the two cases so that overrides
+ ;; don't hide real conflicts.
+ (puthash key (gethash key override) table)
+ (display-warning 'smie (format "Conflict: %s %s/%s %s" x old val y)))
+ (puthash key val table))))
+
+(defun smie-precs-precedence-table (precs)
+ "Compute a 2D precedence table from a list of precedences.
+PRECS should be a list, sorted by precedence (e.g. \"+\" will
+come before \"*\"), of elements of the form \(left OP ...)
+or (right OP ...) or (nonassoc OP ...) or (assoc OP ...). All operators in
+one of those elements share the same precedence level and associativity."
+ (let ((prec2-table (make-hash-table :test 'equal)))
+ (dolist (prec precs)
+ (dolist (op (cdr prec))
+ (let ((selfrule (cdr (assq (car prec)
+ '((left . >) (right . <) (assoc . =))))))
+ (when selfrule
+ (dolist (other-op (cdr prec))
+ (smie-set-prec2tab prec2-table op other-op selfrule))))
+ (let ((op1 '<) (op2 '>))
+ (dolist (other-prec precs)
+ (if (eq prec other-prec)
+ (setq op1 '> op2 '<)
+ (dolist (other-op (cdr other-prec))
+ (smie-set-prec2tab prec2-table op other-op op2)
+ (smie-set-prec2tab prec2-table other-op op op1)))))))
+ prec2-table))
+
+(defun smie-merge-prec2s (&rest tables)
+ (if (null (cdr tables))
+ (car tables)
+ (let ((prec2 (make-hash-table :test 'equal)))
+ (dolist (table tables)
+ (maphash (lambda (k v)
+ (smie-set-prec2tab prec2 (car k) (cdr k) v))
+ table))
+ prec2)))
+
+(defun smie-bnf-precedence-table (bnf &rest precs)
+ (let ((nts (mapcar 'car bnf)) ;Non-terminals
+ (first-ops-table ())
+ (last-ops-table ())
+ (first-nts-table ())
+ (last-nts-table ())
+ (prec2 (make-hash-table :test 'equal))
+ (override (apply 'smie-merge-prec2s
+ (mapcar 'smie-precs-precedence-table precs)))
+ again)
+ (dolist (rules bnf)
+ (let ((nt (car rules))
+ (last-ops ())
+ (first-ops ())
+ (last-nts ())
+ (first-nts ()))
+ (dolist (rhs (cdr rules))
+ (assert (consp rhs))
+ (if (not (member (car rhs) nts))
+ (pushnew (car rhs) first-ops)
+ (pushnew (car rhs) first-nts)
+ (when (consp (cdr rhs))
+ ;; If the first is not an OP we add the second (which
+ ;; should be an OP if BNF is an "operator grammar").
+ ;; Strictly speaking, this should only be done if the
+ ;; first is a non-terminal which can expand to a phrase
+ ;; without any OP in it, but checking doesn't seem worth
+ ;; the trouble, and it lets the writer of the BNF
+ ;; be a bit more sloppy by skipping uninteresting base
+ ;; cases which are terminals but not OPs.
+ (assert (not (member (cadr rhs) nts)))
+ (pushnew (cadr rhs) first-ops)))
+ (let ((shr (reverse rhs)))
+ (if (not (member (car shr) nts))
+ (pushnew (car shr) last-ops)
+ (pushnew (car shr) last-nts)
+ (when (consp (cdr shr))
+ (assert (not (member (cadr shr) nts)))
+ (pushnew (cadr shr) last-ops)))))
+ (push (cons nt first-ops) first-ops-table)
+ (push (cons nt last-ops) last-ops-table)
+ (push (cons nt first-nts) first-nts-table)
+ (push (cons nt last-nts) last-nts-table)))
+ ;; Compute all first-ops by propagating the initial ones we have
+ ;; now, according to first-nts.
+ (setq again t)
+ (while (prog1 again (setq again nil))
+ (dolist (first-nts first-nts-table)
+ (let* ((nt (pop first-nts))
+ (first-ops (assoc nt first-ops-table)))
+ (dolist (first-nt first-nts)
+ (dolist (op (cdr (assoc first-nt first-ops-table)))
+ (unless (member op first-ops)
+ (setq again t)
+ (push op (cdr first-ops))))))))
+ ;; Same thing for last-ops.
+ (setq again t)
+ (while (prog1 again (setq again nil))
+ (dolist (last-nts last-nts-table)
+ (let* ((nt (pop last-nts))
+ (last-ops (assoc nt last-ops-table)))
+ (dolist (last-nt last-nts)
+ (dolist (op (cdr (assoc last-nt last-ops-table)))
+ (unless (member op last-ops)
+ (setq again t)
+ (push op (cdr last-ops))))))))
+ ;; Now generate the 2D precedence table.
+ (dolist (rules bnf)
+ (dolist (rhs (cdr rules))
+ (while (cdr rhs)
+ (cond
+ ((member (car rhs) nts)
+ (dolist (last (cdr (assoc (car rhs) last-ops-table)))
+ (smie-set-prec2tab prec2 last (cadr rhs) '> override)))
+ ((member (cadr rhs) nts)
+ (dolist (first (cdr (assoc (cadr rhs) first-ops-table)))
+ (smie-set-prec2tab prec2 (car rhs) first '< override))
+ (if (and (cddr rhs) (not (member (car (cddr rhs)) nts)))
+ (smie-set-prec2tab prec2 (car rhs) (car (cddr rhs))
+ '= override)))
+ (t (smie-set-prec2tab prec2 (car rhs) (cadr rhs) '= override)))
+ (setq rhs (cdr rhs)))))
+ prec2))
+
+(defun smie-prec2-levels (prec2)
+ "Take a 2D precedence table and turn it into an alist of precedence levels.
+PREC2 is a table as returned by `smie-precs-precedence-table' or
+`smie-bnf-precedence-table'."
+ ;; For each operator, we create two "variables" (corresponding to
+ ;; the left and right precedence level), which are represented by
+ ;; cons cells. Those are the vary cons cells that appear in the
+ ;; final `table'. The value of each "variable" is kept in the `car'.
+ (let ((table ())
+ (csts ())
+ (eqs ())
+ tmp x y)
+ ;; From `prec2' we construct a list of constraints between
+ ;; variables (aka "precedence levels"). These can be either
+ ;; equality constraints (in `eqs') or `<' constraints (in `csts').
+ (maphash (lambda (k v)
+ (if (setq tmp (assoc (car k) table))
+ (setq x (cddr tmp))
+ (setq x (cons nil nil))
+ (push (cons (car k) (cons nil x)) table))
+ (if (setq tmp (assoc (cdr k) table))
+ (setq y (cdr tmp))
+ (setq y (cons nil (cons nil nil)))
+ (push (cons (cdr k) y) table))
+ (ecase v
+ (= (push (cons x y) eqs))
+ (< (push (cons x y) csts))
+ (> (push (cons y x) csts))))
+ prec2)
+ ;; First process the equality constraints.
+ (let ((eqs eqs))
+ (while eqs
+ (let ((from (caar eqs))
+ (to (cdar eqs)))
+ (setq eqs (cdr eqs))
+ (if (eq to from)
+ nil ;Nothing to do.
+ (dolist (other-eq eqs)
+ (if (eq from (cdr other-eq)) (setcdr other-eq to))
+ (when (eq from (car other-eq))
+ ;; This can happen because of `assoc' settings in precs
+ ;; or because of a rhs like ("op" foo "op").
+ (setcar other-eq to)))
+ (dolist (cst csts)
+ (if (eq from (cdr cst)) (setcdr cst to))
+ (if (eq from (car cst)) (setcar cst to)))))))
+ ;; Then eliminate trivial constraints iteratively.
+ (let ((i 0))
+ (while csts
+ (let ((rhvs (mapcar 'cdr csts))
+ (progress nil))
+ (dolist (cst csts)
+ (unless (memq (car cst) rhvs)
+ (setq progress t)
+ ;; We could give each var in a given iteration the same value,
+ ;; but we can also give them arbitrarily different values.
+ ;; Basically, these are vars between which there is no
+ ;; constraint (neither equality nor inequality), so
+ ;; anything will do.
+ ;; We give them arbitrary values, which means that we
+ ;; replace the "no constraint" case with either > or <
+ ;; but not =. The reason we do that is so as to try and
+ ;; distinguish associative operators (which will have
+ ;; left = right).
+ (unless (caar cst)
+ (setcar (car cst) i)
+ (incf i))
+ (setq csts (delq cst csts))))
+ (unless progress
+ (error "Can't resolve the precedence table to precedence levels")))
+ (incf i 10))
+ ;; Propagate equalities back to their source.
+ (dolist (eq (nreverse eqs))
+ (assert (or (null (caar eq)) (eq (car eq) (cdr eq))))
+ (setcar (car eq) (cadr eq)))
+ ;; Finally, fill in the remaining vars (which only appeared on the
+ ;; right side of the < constraints).
+ (dolist (x table)
+ ;; When both sides are nil, it means this operator binds very
+ ;; very tight, but it's still just an operator, so we give it
+ ;; the highest precedence.
+ ;; OTOH if only one side is nil, it usually means it's like an
+ ;; open-paren, which is very important for indentation purposes,
+ ;; so we keep it nil, to make it easier to recognize.
+ (unless (or (nth 1 x) (nth 2 x))
+ (setf (nth 1 x) i)
+ (setf (nth 2 x) i))))
+ table))
+
+;;; Parsing using a precedence level table.
+
+(defvar smie-op-levels 'unset
+ "List of token parsing info.
+Each element is of the form (TOKEN LEFT-LEVEL RIGHT-LEVEL).
+Parsing is done using an operator precedence parser.
+LEFT-LEVEL and RIGHT-LEVEL can be either numbers or nil, where nil
+means that this operator does not bind on the corresponding side,
+i.e. a LEFT-LEVEL of nil means this is a token that behaves somewhat like
+an open-paren, whereas a RIGHT-LEVEL of nil would correspond to something
+like a close-paren.")
+
+(defvar smie-forward-token-function 'smie-default-forward-token
+ "Function to scan forward for the next token.
+Called with no argument should return a token and move to its end.
+If no token is found, return nil or the empty string.
+It can return nil when bumping into a parenthesis, which lets SMIE
+use syntax-tables to handle them in efficient C code.")
+
+(defvar smie-backward-token-function 'smie-default-backward-token
+ "Function to scan backward the previous token.
+Same calling convention as `smie-forward-token-function' except
+it should move backward to the beginning of the previous token.")
+
+(defalias 'smie-op-left 'car)
+(defalias 'smie-op-right 'cadr)
+
+(defun smie-default-backward-token ()
+ (forward-comment (- (point)))
+ (buffer-substring (point)
+ (progn (if (zerop (skip-syntax-backward "."))
+ (skip-syntax-backward "w_'"))
+ (point))))
+
+(defun smie-default-forward-token ()
+ (forward-comment (point-max))
+ (buffer-substring (point)
+ (progn (if (zerop (skip-syntax-forward "."))
+ (skip-syntax-forward "w_'"))
+ (point))))
+
+(defun smie-associative-p (toklevels)
+ ;; in "a + b + c" we want to stop at each +, but in
+ ;; "if a then b else c" we don't want to stop at each keyword.
+ ;; To distinguish the two cases, we made smie-prec2-levels choose
+ ;; different levels for each part of "if a then b else c", so that
+ ;; by checking if the left-level is equal to the right level, we can
+ ;; figure out that it's an associative operator.
+ ;; This is not 100% foolproof, tho, since a grammar like
+ ;; (exp ("A" exp "C") ("A" exp "B" exp "C"))
+ ;; will cause "B" to have equal left and right levels, even though
+ ;; it is not an associative operator.
+ ;; A better check would be the check the actual previous operator
+ ;; against this one to see if it's the same, but we'd have to change
+ ;; `levels' to keep a stack of operators rather than only levels.
+ (eq (smie-op-left toklevels) (smie-op-right toklevels)))
+
+(defun smie-next-sexp (next-token next-sexp op-forw op-back halfsexp)
+ "Skip over one sexp.
+NEXT-TOKEN is a function of no argument that moves forward by one
+token (after skipping comments if needed) and returns it.
+NEXT-SEXP is a lower-level function to skip one sexp.
+OP-FORW is the accessor to the forward level of the level data.
+OP-BACK is the accessor to the backward level of the level data.
+HALFSEXP if non-nil, means skip over a partial sexp if needed. I.e. if the
+first token we see is an operator, skip over its left-hand-side argument.
+Possible return values:
+ (FORW-LEVEL POS TOKEN): we couldn't skip TOKEN because its back-level
+ is too high. FORW-LEVEL is the forw-level of TOKEN,
+ POS is its start position in the buffer.
+ (t POS TOKEN): same thing when we bump on the wrong side of a paren.
+ (nil POS TOKEN): we skipped over a paren-like pair.
+ nil: we skipped over an identifier, matched parentheses, ..."
+ (catch 'return
+ (let ((levels ()))
+ (while
+ (let* ((pos (point))
+ (token (funcall next-token))
+ (toklevels (cdr (assoc token smie-op-levels))))
+
+ (cond
+ ((null toklevels)
+ (when (zerop (length token))
+ (condition-case err
+ (progn (goto-char pos) (funcall next-sexp 1) nil)
+ (scan-error (throw 'return (list t (caddr err)))))
+ (if (eq pos (point))
+ ;; We did not move, so let's abort the loop.
+ (throw 'return (list t (point))))))
+ ((null (funcall op-back toklevels))
+ ;; A token like a paren-close.
+ (assert (funcall op-forw toklevels)) ;Otherwise, why mention it?
+ (push (funcall op-forw toklevels) levels))
+ (t
+ (while (and levels (< (funcall op-back toklevels) (car levels)))
+ (setq levels (cdr levels)))
+ (cond
+ ((null levels)
+ (if (and halfsexp (funcall op-forw toklevels))
+ (push (funcall op-forw toklevels) levels)
+ (throw 'return
+ (prog1 (list (or (car toklevels) t) (point) token)
+ (goto-char pos)))))
+ (t
+ (if (and levels (= (funcall op-back toklevels) (car levels)))
+ (setq levels (cdr levels)))
+ (cond
+ ((null levels)
+ (cond
+ ((null (funcall op-forw toklevels))
+ (throw 'return (list nil (point) token)))
+ ((smie-associative-p toklevels)
+ (throw 'return
+ (prog1 (list (or (car toklevels) t) (point) token)
+ (goto-char pos))))
+ ;; We just found a match to the previously pending operator
+ ;; but this new operator is still part of a larger RHS.
+ ;; E.g. we're now looking at the "then" in
+ ;; "if a then b else c". So we have to keep parsing the
+ ;; rest of the construct.
+ (t (push (funcall op-forw toklevels) levels))))
+ (t
+ (if (funcall op-forw toklevels)
+ (push (funcall op-forw toklevels) levels))))))))
+ levels)
+ (setq halfsexp nil)))))
+
+(defun smie-backward-sexp (&optional halfsexp)
+ "Skip over one sexp.
+HALFSEXP if non-nil, means skip over a partial sexp if needed. I.e. if the
+first token we see is an operator, skip over its left-hand-side argument.
+Possible return values:
+ (LEFT-LEVEL POS TOKEN): we couldn't skip TOKEN because its right-level
+ is too high. LEFT-LEVEL is the left-level of TOKEN,
+ POS is its start position in the buffer.
+ (t POS TOKEN): same thing but for an open-paren or the beginning of buffer.
+ (nil POS TOKEN): we skipped over a paren-like pair.
+ nil: we skipped over an identifier, matched parentheses, ..."
+ (smie-next-sexp
+ (indirect-function smie-backward-token-function)
+ (indirect-function 'backward-sexp)
+ (indirect-function 'smie-op-left)
+ (indirect-function 'smie-op-right)
+ halfsexp))
+
+(defun smie-forward-sexp (&optional halfsexp)
+ "Skip over one sexp.
+HALFSEXP if non-nil, means skip over a partial sexp if needed. I.e. if the
+first token we see is an operator, skip over its left-hand-side argument.
+Possible return values:
+ (RIGHT-LEVEL POS TOKEN): we couldn't skip TOKEN because its left-level
+ is too high. RIGHT-LEVEL is the right-level of TOKEN,
+ POS is its end position in the buffer.
+ (t POS TOKEN): same thing but for an open-paren or the beginning of buffer.
+ (nil POS TOKEN): we skipped over a paren-like pair.
+ nil: we skipped over an identifier, matched parentheses, ..."
+ (smie-next-sexp
+ (indirect-function smie-forward-token-function)
+ (indirect-function 'forward-sexp)
+ (indirect-function 'smie-op-right)
+ (indirect-function 'smie-op-left)
+ halfsexp))
+
+(defun smie-backward-sexp-command (&optional n)
+ "Move backward through N logical elements."
+ (interactive "p")
+ (if (< n 0)
+ (smie-forward-sexp-command (- n))
+ (let ((forward-sexp-function nil))
+ (while (> n 0)
+ (decf n)
+ (let ((pos (point))
+ (res (smie-backward-sexp 'halfsexp)))
+ (if (and (car res) (= pos (point)) (not (bolp)))
+ (signal 'scan-error
+ (list "Containing expression ends prematurely"
+ (cadr res) (cadr res)))
+ nil))))))
+
+(defun smie-forward-sexp-command (&optional n)
+ "Move forward through N logical elements."
+ (interactive "p")
+ (if (< n 0)
+ (smie-backward-sexp-command (- n))
+ (let ((forward-sexp-function nil))
+ (while (> n 0)
+ (decf n)
+ (let ((pos (point))
+ (res (smie-forward-sexp 'halfsexp)))
+ (if (and (car res) (= pos (point)) (not (bolp)))
+ (signal 'scan-error
+ (list "Containing expression ends prematurely"
+ (cadr res) (cadr res)))
+ nil))))))
+
+;;; The indentation engine.
+
+(defcustom smie-indent-basic 4
+ "Basic amount of indentation."
+ :type 'integer)
+
+(defvar smie-indent-rules 'unset
+ ;; TODO: For SML, we need more rule formats, so as to handle
+ ;; structure Foo =
+ ;; Bar (toto)
+ ;; and
+ ;; structure Foo =
+ ;; struct ... end
+ ;; I.e. the indentation after "=" depends on the parent ("structure")
+ ;; as well as on the following token ("struct").
+ "Rules of the following form.
+\((:before . TOK) . OFFSET-RULES) how to indent TOK itself.
+\(TOK . OFFSET-RULES) how to indent right after TOK.
+\((T1 . T2) . OFFSET) how to indent token T2 w.r.t T1.
+\((t . TOK) . OFFSET) how to indent TOK with respect to its parent.
+\(list-intro . TOKENS) declare TOKENS as being followed by what may look like
+ a funcall but is just a sequence of expressions.
+\(t . OFFSET) basic indentation step.
+\(args . OFFSET) indentation of arguments.
+
+OFFSET-RULES is a list of elements which can each either be:
+
+\(:hanging . OFFSET-RULES) if TOK is hanging, use OFFSET-RULES.
+\(:parent PARENT . OFFSET-RULES) if TOK's parent is PARENT, use OFFSET-RULES.
+\(:next TOKEN . OFFSET-RULES) if TOK is followed by TOKEN, use OFFSET-RULES.
+\(:prev TOKEN . OFFSET-RULES) if TOK is preceded by TOKEN, use OFFSET-RULES.
+a number the offset to use.
+`point' align with the token.
+`parent' align with the parent.
+
+A nil offset for indentation after a token defaults to `smie-indent-basic'.")
+
+(defun smie-indent-hanging-p ()
+ ;; A hanging keyword is one that's at the end of a line except it's not at
+ ;; the beginning of a line.
+ (and (save-excursion
+ (when (zerop (length (funcall smie-forward-token-function)))
+ ;; Could be an open-paren.
+ (forward-char 1))
+ (skip-chars-forward " \t")
+ (eolp))
+ (not (smie-bolp))))
+
+(defun smie-bolp ()
+ (save-excursion (skip-chars-backward " \t") (bolp)))
+
+(defun smie-indent-offset (elem)
+ (or (cdr (assq elem smie-indent-rules))
+ (cdr (assq t smie-indent-rules))
+ smie-indent-basic))
+
+(defun smie-indent-offset-rule (tokinfo &optional after)
+ "Apply the OFFSET-RULES in TOKINFO.
+Point is expected to be right in front of the token corresponding to TOKINFO.
+If computing the indentation after the token, then AFTER is the position
+after the token."
+ (let ((rules (cdr tokinfo))
+ parent next prev
+ offset)
+ (while (consp rules)
+ (let ((rule (pop rules)))
+ (cond
+ ((not (consp rule)) (setq offset rule))
+ ((eq (car rule) :hanging)
+ (when (smie-indent-hanging-p)
+ (setq rules (cdr rule))))
+ ((eq (car rule) :prev)
+ (unless prev
+ (save-excursion
+ (setq prev (smie-indent-backward-token))))
+ (when (equal (car prev) (cadr rule))
+ (setq rules (cddr rule))))
+ ((eq (car rule) :next)
+ (unless next
+ (unless after
+ (error "Can't use :next in :before indentation rules"))
+ (save-excursion
+ (goto-char after)
+ (setq next (smie-indent-forward-token))))
+ (when (equal (car next) (cadr rule))
+ (setq rules (cddr rule))))
+ ((eq (car rule) :parent)
+ (unless parent
+ (save-excursion
+ (if after (goto-char after))
+ (setq parent (smie-backward-sexp 'halfsexp))))
+ (when (equal (nth 2 parent) (cadr rule))
+ (setq rules (cddr rule))))
+ (t (error "Unknown rule %s for indentation of %s"
+ rule (car tokinfo))))))
+ offset))
+
+(defun smie-indent-forward-token ()
+ "Skip token forward and return it, along with its levels."
+ (let ((tok (funcall smie-forward-token-function)))
+ (cond
+ ((< 0 (length tok)) (assoc tok smie-op-levels))
+ ((looking-at "\\s(")
+ (forward-char 1)
+ (list (buffer-substring (1- (point)) (point)) nil 0)))))
+
+(defun smie-indent-backward-token ()
+ "Skip token backward and return it, along with its levels."
+ (let ((tok (funcall smie-backward-token-function)))
+ (cond
+ ((< 0 (length tok)) (assoc tok smie-op-levels))
+ ;; 4 == Open paren syntax.
+ ((eq 4 (syntax-class (syntax-after (1- (point)))))
+ (forward-char -1)
+ (list (buffer-substring (point) (1+ (point))) nil 0)))))
+
+(defun smie-indent-virtual ()
+ ;; We used to take an optional arg (with value :not-hanging) to specify that
+ ;; we should only use (smie-indent-calculate) if we're looking at a hanging
+ ;; keyword. This was a bad idea, because the virtual indent of a position
+ ;; should not depend on the caller, since it leads to situations where two
+ ;; dependent indentations get indented differently.
+ "Compute the virtual indentation to use for point.
+This is used when we're not trying to indent point but just
+need to compute the column at which point should be indented
+in order to figure out the indentation of some other (further down) point."
+ ;; Trust pre-existing indentation on other lines.
+ (if (smie-bolp) (current-column) (smie-indent-calculate)))
+
+(defun smie-indent-fixindent ()
+ ;; Obey the `fixindent' special comment.
+ (and (smie-bolp)
+ (save-excursion
+ (comment-normalize-vars)
+ (re-search-forward (concat comment-start-skip
+ "fixindent"
+ comment-end-skip)
+ ;; 1+ to account for the \n comment termination.
+ (1+ (line-end-position)) t))
+ (current-column)))
+
+(defun smie-indent-bob ()
+ ;; Start the file at column 0.
+ (save-excursion
+ (forward-comment (- (point)))
+ (if (bobp) 0)))
+
+(defun smie-indent-close ()
+ ;; Align close paren with opening paren.
+ (save-excursion
+ ;; (forward-comment (point-max))
+ (when (looking-at "\\s)")
+ (while (not (zerop (skip-syntax-forward ")")))
+ (skip-chars-forward " \t"))
+ (condition-case nil
+ (progn
+ (backward-sexp 1)
+ (smie-indent-virtual)) ;:not-hanging
+ (scan-error nil)))))
+
+(defun smie-indent-keyword ()
+ ;; Align closing token with the corresponding opening one.
+ ;; (e.g. "of" with "case", or "in" with "let").
+ (save-excursion
+ (let* ((pos (point))
+ (toklevels (smie-indent-forward-token))
+ (token (pop toklevels)))
+ (if (null (car toklevels))
+ ;; Different case:
+ ;; - smie-bolp: "indent according to others".
+ ;; - common hanging: "indent according to others".
+ ;; - SML-let hanging: "indent like parent".
+ ;; - if-after-else: "indent-like parent".
+ ;; - middle-of-line: "trust current position".
+ (cond
+ ((null (cdr toklevels)) nil) ;Not a keyword.
+ ((smie-bolp)
+ ;; For an open-paren-like thingy at BOL, always indent only
+ ;; based on other rules (typically smie-indent-after-keyword).
+ nil)
+ (t
+ (let* ((tokinfo (or (assoc (cons :before token) smie-indent-rules)
+ ;; By default use point unless we're hanging.
+ (cons (cons :before token)
+ '((:hanging nil) point))))
+ (after (prog1 (point) (goto-char pos)))
+ (offset (smie-indent-offset-rule tokinfo)))
+ (cond
+ ((eq offset 'point) (current-column))
+ ((eq offset 'parent)
+ (let ((parent (smie-backward-sexp 'halfsexp)))
+ (if parent (goto-char (cadr parent))))
+ (smie-indent-virtual))
+ ((eq offset nil) nil)
+ (t (error "Unhandled offset %s in %s"
+ offset (cons :before token)))))))
+
+ ;; FIXME: This still looks too much like black magic!!
+ ;; FIXME: Rather than a bunch of rules like (PARENT . TOKEN), we
+ ;; want a single rule for TOKEN with different cases for each PARENT.
+ (let ((res (smie-backward-sexp 'halfsexp)) tmp)
+ (cond
+ ((not (or (< (point) pos)
+ (and (cadr res) (< (cadr res) pos))))
+ ;; If we didn't move at all, that means we didn't really skip
+ ;; what we wanted.
+ nil)
+ ((eq (car res) (car toklevels))
+ ;; We bumped into a same-level operator. align with it.
+ (goto-char (cadr res))
+ ;; Don't use (smie-indent-virtual :not-hanging) here, because we
+ ;; want to jump back over a sequence of same-level ops such as
+ ;; a -> b -> c
+ ;; -> d
+ ;; So as to align with the earliest appropriate place.
+ (smie-indent-virtual))
+ ((setq tmp (assoc (cons (caddr res) token)
+ smie-indent-rules))
+ (goto-char (cadr res))
+ (+ (cdr tmp) (smie-indent-virtual))) ;:not-hanging
+ ;; FIXME: The rules ((t . TOK) . OFFSET) either indent
+ ;; relative to "before the parent" or "after the parent",
+ ;; depending on details of the grammar.
+ ((null (car res))
+ (assert (eq (point) (cadr res)))
+ (goto-char (cadr res))
+ (+ (or (cdr (assoc (cons t token) smie-indent-rules)) 0)
+ (smie-indent-virtual))) ;:not-hanging
+ ((and (= (point) pos) (smie-bolp))
+ ;; Since we started at BOL, we're not computing a virtual
+ ;; indentation, and we're still at the starting point, so the
+ ;; next (default) rule can't be used since it uses `current-column'
+ ;; which would cause. indentation to depend on itself.
+ ;; We could just return nil, but OTOH that's not good enough in
+ ;; some cases. Instead, we want to combine the offset-rules for
+ ;; the current token with the offset-rules of the previous one.
+ (+ (or (cdr (assoc (cons t token) smie-indent-rules)) 0)
+ ;; FIXME: This is odd. Can't we make it use
+ ;; smie-indent-(calculate|virtual) somehow?
+ (smie-indent-after-keyword)))
+ (t
+ (+ (or (cdr (assoc (cons t token) smie-indent-rules)) 0)
+ (current-column)))))))))
+
+(defun smie-indent-comment ()
+ ;; Indentation of a comment.
+ (and (looking-at comment-start-skip)
+ (save-excursion
+ (forward-comment (point-max))
+ (skip-chars-forward " \t\r\n")
+ (smie-indent-calculate))))
+
+(defun smie-indent-comment-continue ()
+ ;; indentation of comment-continue lines.
+ (let ((continue (and comment-continue
+ (comment-string-strip comment-continue t t))))
+ (and (< 0 (length continue))
+ (looking-at (regexp-quote continue)) (nth 4 (syntax-ppss))
+ (let ((ppss (syntax-ppss)))
+ (save-excursion
+ (forward-line -1)
+ (if (<= (point) (nth 8 ppss))
+ (progn (goto-char (1+ (nth 8 ppss))) (current-column))
+ (skip-chars-forward " \t")
+ (if (looking-at (regexp-quote continue))
+ (current-column))))))))
+
+(defun smie-indent-after-keyword ()
+ ;; Indentation right after a special keyword.
+ (save-excursion
+ (let* ((pos (point))
+ (toklevel (smie-indent-backward-token))
+ (tok (car toklevel))
+ (tokinfo (assoc tok smie-indent-rules)))
+ (if (and toklevel (null (cadr toklevel)) (null tokinfo))
+ (setq tokinfo (list (car toklevel))))
+ ;; (if (and tokinfo (null toklevel))
+ ;; (error "Token %S has indent rule but has no parsing info" tok))
+ (when toklevel
+ (let ((offset
+ (cond
+ (tokinfo (or (smie-indent-offset-rule tokinfo pos)
+ (smie-indent-offset t)))
+ ;; The default indentation after a keyword/operator
+ ;; is 0 for infix and t for prefix.
+ ;; Using the BNF syntax, we could come up with
+ ;; better defaults, but we only have the
+ ;; precedence levels here.
+ ((null (cadr toklevel)) (smie-indent-offset t))
+ (t 0))))
+ ;; For indentation after "(let" in SML-mode, we end up accumulating
+ ;; the offset of "(" and the offset of "let", so we use `min' to try
+ ;; and get it right either way.
+ (+ (min (smie-indent-virtual) (current-column)) offset))))))
+
+(defun smie-indent-exps ()
+ ;; Indentation of sequences of simple expressions without
+ ;; intervening keywords or operators. E.g. "a b c" or "g (balbla) f".
+ ;; Can be a list of expressions or a function call.
+ ;; If it's a function call, the first element is special (it's the
+ ;; function). We distinguish function calls from mere lists of
+ ;; expressions based on whether the preceding token is listed in
+ ;; the `list-intro' entry of smie-indent-rules.
+ ;;
+ ;; TODO: to indent Lisp code, we should add a way to specify
+ ;; particular indentation for particular args depending on the
+ ;; function (which would require always skipping back until the
+ ;; function).
+ ;; TODO: to indent C code, such as "if (...) {...}" we might need
+ ;; to add similar indentation hooks for particular positions, but
+ ;; based on the preceding token rather than based on the first exp.
+ (save-excursion
+ (let ((positions nil)
+ arg)
+ (while (and (null (car (smie-backward-sexp)))
+ (push (point) positions)
+ (not (smie-bolp))))
+ (save-excursion
+ ;; Figure out if the atom we just skipped is an argument rather
+ ;; than a function.
+ (setq arg (or (null (car (smie-backward-sexp)))
+ (member (funcall smie-backward-token-function)
+ (cdr (assoc 'list-intro smie-indent-rules))))))
+ (cond
+ ((null positions)
+ ;; We're the first expression of the list. In that case, the
+ ;; indentation should be (have been) determined by its context.
+ nil)
+ (arg
+ ;; There's a previous element, and it's not special (it's not
+ ;; the function), so let's just align with that one.
+ (goto-char (car positions))
+ (current-column))
+ ((cdr positions)
+ ;; We skipped some args plus the function and bumped into something.
+ ;; Align with the first arg.
+ (goto-char (cadr positions))
+ (current-column))
+ (positions
+ ;; We're the first arg.
+ (goto-char (car positions))
+ (+ (smie-indent-offset 'args)
+ ;; We used to use (smie-indent-virtual), but that
+ ;; doesn't seem right since it might then indent args less than
+ ;; the function itself.
+ (current-column)))))))
+
+(defvar smie-indent-functions
+ '(smie-indent-fixindent smie-indent-bob smie-indent-close smie-indent-comment
+ smie-indent-comment-continue smie-indent-keyword smie-indent-after-keyword
+ smie-indent-exps)
+ "Functions to compute the indentation.
+Each function is called with no argument, shouldn't move point, and should
+return either nil if it has no opinion, or an integer representing the column
+to which that point should be aligned, if we were to reindent it.")
+
+(defun smie-indent-calculate ()
+ "Compute the indentation to use for point."
+ (run-hook-with-args-until-success 'smie-indent-functions))
+
+(defun smie-indent-line ()
+ "Indent current line using the SMIE indentation engine."
+ (interactive)
+ (let* ((savep (point))
+ (indent (condition-case nil
+ (save-excursion
+ (forward-line 0)
+ (skip-chars-forward " \t")
+ (if (>= (point) savep) (setq savep nil))
+ (or (smie-indent-calculate) 0))
+ (error 0))))
+ (if (not (numberp indent))
+ ;; If something funny is used (e.g. `noindent'), return it.
+ indent
+ (if (< indent 0) (setq indent 0)) ;Just in case.
+ (if savep
+ (save-excursion (indent-line-to indent))
+ (indent-line-to indent)))))
+
+;;;###autoload
+(defun smie-setup (op-levels indent-rules)
+ (set (make-local-variable 'smie-indent-rules) indent-rules)
+ (set (make-local-variable 'smie-op-levels) op-levels)
+ (set (make-local-variable 'indent-line-function) 'smie-indent-line))
+
+
+(provide 'smie)
+;;; smie.el ends here