You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

348 lines
13 KiB

;;; elixir-smie.el --- Structural syntax support for elixir-mode
;; Copyright 2011-2015 secondplanet
;; 2013-2015 Samuel Tonini, Matt DeBoard, Andreas Fuchs
;; This file is not a part of GNU Emacs.
;; This program 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 2, or (at your option)
;; any later version.
;; This program 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, write to the Free Software
;; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
;;; Commentary:
;; Structural syntax support for elixir-mode
;;; Code:
(require 'smie)
;; HACK: Patch for Emacs 24.3 smie that fix
;; https://github.com/elixir-lang/emacs-elixir/issues/107.
;;
;; defadvice is used to change the behavior only for elixir-mode.
;; Definition of advice is a definition of corresponding function
;; in Emacs 24.4.
(when (and (= 24 emacs-major-version)
(= 3 emacs-minor-version))
(defadvice smie-rule-parent (around elixir-mode-patch activate)
(if (not (eq major-mode 'elixir-mode))
(progn ad-do-it)
(setq ad-return-value
(save-excursion
(goto-char (cadr (smie-indent--parent)))
(cons 'column
(+ (or offset 0)
(smie-indent-virtual)))))))
(defadvice smie-indent-comment (around elixir-mode-patch activate)
(if (not (eq major-mode 'elixir-mode))
(progn ad-do-it)
(setq ad-return-value
(and (smie-indent--bolp)
(let ((pos (point)))
(save-excursion
(beginning-of-line)
(and (re-search-forward comment-start-skip (line-end-position) t)
(eq pos (or (match-end 1) (match-beginning 0))))))
(save-excursion
(forward-comment (point-max))
(skip-chars-forward " \t\r\n")
(unless
(save-excursion
(let ((next (funcall smie-forward-token-function)))
(or (if (zerop (length next))
(or (eobp) (eq (car (syntax-after (point))) 5)))
(rassoc next smie-closer-alist))))
(smie-indent-calculate))))))))
;; FIXME: This is me being lazy. CL is a compile-time dep only.
;; (But for now, there is no real file-compilation plot, so let's
;; scrape by with the runtime dep.)
(require 'cl)
(defvar elixir-smie-verbose-p nil
"Emit context information about the current syntax state.")
(defvar elixir-mode-syntax-table
(let ((table (make-syntax-table)))
;; Note that ?_ might be better as class "_", but either seems to
;; work:
(modify-syntax-entry ?_ "_" table)
(modify-syntax-entry ?? "w" table)
(modify-syntax-entry ?~ "w" table)
(modify-syntax-entry ?! "_" table)
(modify-syntax-entry ?' "\"'" table)
(modify-syntax-entry ?\" "\"\"" table)
(modify-syntax-entry ?# "<" table)
(modify-syntax-entry ?\n ">" table)
(modify-syntax-entry ?\( "()" table)
(modify-syntax-entry ?\) ")(" table)
(modify-syntax-entry ?\{ "(}" table)
(modify-syntax-entry ?\} "){" table)
(modify-syntax-entry ?\[ "(]" table)
(modify-syntax-entry ?\] ")[" table)
(modify-syntax-entry ?: "_" table)
(modify-syntax-entry ?@ "_" table)
table)
"Elixir mode syntax table.")
(defconst elixir-smie-grammar
(smie-prec2->grammar
(smie-merge-prec2s
(smie-bnf->prec2
'((id)
(statements (statement)
(statement ";" statements))
(statement ("def" non-block-expr "do" statements "end")
(non-block-expr "fn" match-statements "end")
(non-block-expr "do" statements "end")
("if" non-block-expr "do" statements "else" statements "end")
("if" non-block-expr "do" statements "end")
("if" non-block-expr "COMMA" "do:" non-block-expr)
("if" non-block-expr "COMMA"
"do:" non-block-expr "COMMA"
"else:" non-block-expr)
("try" "do" statements "after" statements "end")
("try" "do" statements "catch" match-statements "end")
("try" "do" statements "rescue" match-statements "end")
("try" "do" statements "end")
("case" non-block-expr "do" match-statements "end"))
(non-block-expr (non-block-expr "OP" non-block-expr)
(non-block-expr "COMMA" non-block-expr)
("(" non-block-expr ")")
("{" non-block-expr "}")
("[" non-block-expr "]")
("STRING"))
(match-statements (match-statement "MATCH-STATEMENT-DELIMITER"
match-statements)
(match-statement))
(match-statement (non-block-expr "->" statements)))
'((assoc "if" "do:" "else:")
(assoc "COMMA")
(left "OP")))
(smie-precs->prec2
'((left "||")
(left "&&")
(nonassoc "=~" "===" "!==" "==" "!=" "<=" ">=" "<" ">")
(left "+" "-" "<<<" ">>>" "^^^" "~~~" "&&&" "|||")
(left "*" "/"))))))
(defvar elixir-smie--operator-regexp
(rx (or "<<<" ">>>" "^^^" "~~~" "&&&" "|||" "===" "!==" "==" "!=" "<="
"=" ">=" "<" ">" "&&" "||" "<>" "++" "--" "//" "/>" "=~" "|>")))
(defvar elixir-smie--binary-sequence-regexp
(rx (or "<<" ">>")))
(defvar elixir-smie--block-operator-regexp
(rx "->" (0+ nonl)))
(defvar elixir-smie--spaces-til-eol-regexp
(rx (and (1+ space) eol))
"Regex representing one or more whitespace characters concluding with eol.")
(defvar elixir-smie--comment-regexp
(rx (and (0+ space) "#" (0+ not-newline)))
"Regex matching comments.")
(defvar elixir-smie-indent-basic 2)
(defmacro elixir-smie-debug (message &rest format-args)
`(progn
(when elixir-smie-verbose-p
(message (format ,message ,@format-args)))
nil))
(defun elixir-smie--implicit-semi-p ()
(not (or (memq (char-before) '(?\{ ?\[))
(looking-back elixir-smie--operator-regexp (- (point) 3) t))))
(defun elixir-smie--semi-ends-match ()
"Return non-nil if the current line concludes a match block."
(when (not (eobp))
(save-excursion
;; Warning: Recursion.
;; This is easy though.
;; 1. If we're at a blank line, move forward a character. This takes us to
;; the next line.
;; 2. If we're not at the end of the buffer, call this function again.
;; (Otherwise, return nil.)
;; The point here is that we want to treat blank lines as a single semi-
;; colon when it comes to detecting the end of match statements. This could
;; also be handled by a `while' expression or some other looping mechanism.
(cl-flet ((self-call ()
(if (< (point) (point-max))
(elixir-smie--semi-ends-match)
nil)))
(cond
((and (eolp) (bolp))
(forward-char)
(self-call))
((looking-at elixir-smie--spaces-til-eol-regexp)
(move-beginning-of-line 2)
(self-call))
;; And if we're NOT on a blank line, move to the end of the line, and see
;; if we're looking back at a block operator.
(t (move-end-of-line 1)
(looking-back elixir-smie--block-operator-regexp)))))))
(defun elixir-smie--same-line-as-parent (parent-pos child-pos)
"Return non-nil if `child-pos' is on same line as `parent-pos'."
(= (line-number-at-pos parent-pos) (line-number-at-pos child-pos)))
(defun elixir-smie-forward-token ()
(cond
;; If there is nothing but whitespace between the last token and eol, emit
;; a semicolon.
((looking-at elixir-smie--spaces-til-eol-regexp)
(goto-char (match-end 0))
";")
((and (or (looking-at elixir-smie--comment-regexp)
(looking-at "[\n#]"))
(elixir-smie--implicit-semi-p))
(when (not (save-excursion
(forward-comment 1)
(eobp)))
(if (eolp) (forward-char 1) (forward-comment 1)))
;; Note: `elixir-smie--semi-ends-match' will be called when the point is at
;; the beginning of a new line. Keep that in mind.
(if (elixir-smie--semi-ends-match)
"MATCH-STATEMENT-DELIMITER"
";"))
((looking-at elixir-smie--block-operator-regexp)
(goto-char (match-end 0))
"->")
((looking-at elixir-smie--operator-regexp)
(goto-char (match-end 0))
"OP")
(t (smie-default-forward-token))))
(defun elixir-smie-backward-token ()
(let ((pos (point)))
(forward-comment (- (point)))
(cond
((and (> pos (line-end-position))
(elixir-smie--implicit-semi-p))
(if (elixir-smie--semi-ends-match)
"MATCH-STATEMENT-DELIMITER"
";"))
((looking-back elixir-smie--block-operator-regexp (- (point) 3) t)
(goto-char (match-beginning 0))
"->")
((looking-back elixir-smie--binary-sequence-regexp (- (point) 3) t)
(goto-char (match-beginning 0))
"OP")
((looking-back elixir-smie--operator-regexp (- (point) 3) t)
(goto-char (match-beginning 0))
"OP")
(t (smie-default-backward-token)))))
(defun verbose-elixir-smie-rules (kind token)
(let ((value (elixir-smie-rules kind token)))
(elixir-smie-debug "%s '%s'; sibling-p:%s parent:%s prev-is-OP:%s hanging:%s == %s" kind token
(ignore-errors (smie-rule-sibling-p))
(ignore-errors smie--parent)
(ignore-errors (smie-rule-prev-p "OP"))
(ignore-errors (smie-rule-hanging-p))
value)
value))
(defun elixir-smie-rules (kind token)
(pcase (cons kind token)
(`(:before . "OP")
(when (and (not (smie-rule-hanging-p))
(smie-rule-prev-p "OP"))
-2))
(`(:after . "OP")
(cond
((smie-rule-sibling-p) nil)
((smie-rule-hanging-p) (smie-rule-parent elixir-smie-indent-basic))
(t (smie-rule-parent))))
(`(:before . "MATCH-STATEMENT-DELIMITER")
(cond
((and (not (smie-rule-sibling-p))
(smie-rule-hanging-p))
(smie-rule-parent elixir-smie-indent-basic))))
(`(:before . "fn")
(smie-rule-parent))
(`(:before . "end")
(smie-rule-parent))
;; Closing paren on the other line
(`(:before . "(")
(smie-rule-parent))
(`(:before . "[")
(cond
((smie-rule-hanging-p)
(smie-rule-parent))))
(`(:after . "[")
(cond
((smie-rule-hanging-p)
(smie-rule-parent elixir-smie-indent-basic))))
(`(:before . "->")
(cond
((smie-rule-hanging-p)
(smie-rule-parent elixir-smie-indent-basic))))
(`(:after . "->")
(cond
;; This first condition is kind of complicated so I'll try to make this
;; comment as clear as possible.
;; "If `->' is the last thing on the line, and its parent token
;; is `fn' ..."
((and (smie-rule-hanging-p)
(smie-rule-parent-p "fn"))
;; "... and if:
;; 1. `smie--parent' is non-nil
;; 2. the `->' token in question is on the same line as its parent (if
;; the logic has gotten this far, its parent will be `fn')
;; ... then indent the line after the `->' aligned with the
;; parent, offset by `elixir-smie-indent-basic'."
(if (and smie--parent (elixir-smie--same-line-as-parent
(nth 1 smie--parent)
(point)))
(smie-rule-parent elixir-smie-indent-basic)
elixir-smie-indent-basic))
;; Otherwise, if just indent by two.
((smie-rule-hanging-p)
(cond
((smie-rule-parent-p "after" "catch" "do" "rescue" "try")
elixir-smie-indent-basic)
(t
(smie-rule-parent elixir-smie-indent-basic))))))
(`(:before . ";")
(cond
((smie-rule-parent-p "after" "catch" "def" "defmodule" "defp" "do" "else"
"fn" "if" "rescue" "try" "unless")
(smie-rule-parent elixir-smie-indent-basic))))
(`(:after . ";")
(cond
((smie-rule-parent-p "def")
(smie-rule-parent))
((smie-rule-parent-p "if")
(smie-rule-parent))
((and (smie-rule-parent-p "(")
(save-excursion
(goto-char (cadr smie--parent))
(smie-rule-hanging-p)))
(smie-rule-parent elixir-smie-indent-basic))))))
(provide 'elixir-smie)
;;; elixir-smie.el ends here