Permalink
Fetching contributors…
Cannot retrieve contributors at this time
905 lines (849 sloc) 31.6 KB
;;; 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) ; Simple minded indentation engine
(require 'cl-lib) ; `cl-flet'
;; 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))))))))
(defun elixir-smie-looking-around (back at)
"Check if looking backwards at BACK and forward at AT."
(and (looking-at-p at) (looking-back back)))
;; Declare variable that we need from the smie package
(defvar smie--parent)
(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 ?- "." 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 ?? "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 (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")
("for" non-block-expr "COMMA" "do:" non-block-expr)
("for" non-block-expr "COMMA" "into:" non-block-expr "COMMA" "do" statements "end")
("for" non-block-expr "COMMA" "into:" non-block-expr "COMMA" "do:" non-block-expr)
("with" non-block-expr "do" statements "else" statements "end")
("with" non-block-expr "do:" non-block-expr "COMMA" "else:" non-block-expr))
(non-block-expr (non-block-expr "OP" 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--oneline-def-operator-regexp
(rx "do:" (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-current-line-contains-built-in-keyword-p ()
"Return non-nil if the current line contains built in keywords with a `.'"
(save-excursion
(beginning-of-line)
(looking-at ".+\\.\\(case\\|try\\|if\\|rescue\\)")))
(defun elixir-smie-last-line-end-with-block-operator-p ()
"Return non-nil if the previous line ends with a `->' operator."
(save-excursion
(forward-line -1)
(move-end-of-line 1)
(looking-back elixir-smie--block-operator-regexp (- (point) 3) t)))
(defun elixir-smie-last-line-start-with-block-operator-p ()
(save-excursion
(forward-line -1)
(beginning-of-line)
(looking-at "^\s+->.+$")))
(defun elixir-smie-current-line-start-with-pipe-operator-p ()
(save-excursion
(beginning-of-line)
(looking-at "^\s*|>.+$")))
(defun elixir-smie-last-line-is-assignment-p ()
(save-excursion
(forward-line -1)
(beginning-of-line)
(looking-at "^.+=.+$")))
(defun elixir-smie-last-line-start-with-pipe-operator-p ()
(save-excursion
(forward-line -1)
(beginning-of-line)
(looking-at "^\s*|>.+$")))
(defun elixir-smie-line-starts-with-do-colon-p ()
(save-excursion
(beginning-of-line)
(looking-at "^\s+do:")))
(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)
(forward-char)
(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)
(and (looking-back elixir-smie--block-operator-regexp)
(not (looking-back ".+fn.+")))))))))
(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))
(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"
(if (and (looking-at ".+,$")
(not (> (nth 0 (syntax-ppss)) 0)))
"COMMA"
";")))
((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
(let ((token (smie-default-forward-token)))
(unless (or (elixir-smie-empty-string-p token)
(elixir-smie--at-dot-call))
token)))))
(defun elixir-smie--at-dot-call ()
(and (eq ?w (char-syntax (following-char)))
(eq (char-before) ?.)
(not (eq (char-before (1- (point))) ?.))))
(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"
(if (and (looking-back ",$" (- (point) 3) t)
(not (> (nth 0 (syntax-ppss)) 0)))
"COMMA"
";")))
((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 (let ((token (smie-default-backward-token)))
(unless (or (elixir-smie-empty-string-p token)
(elixir-smie--at-dot-call))
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)
(`(:list-intro . ";")
-4)
(`(:list-intro . nil)
-4)
(`(:elem . args)
-4)
(`(:before . "COMMA")
(cond
((and (smie-rule-parent-p "with")
(smie-rule-hanging-p))
(smie-rule-parent 5))
((and (smie-rule-parent-p ";")
(smie-rule-hanging-p))
(smie-rule-parent))
((and (smie-rule-parent-p "COMMA")
(smie-rule-hanging-p))
(if (save-excursion
(forward-line 1)
(move-beginning-of-line 1)
(looking-at "^.+do:.+$"))
(smie-rule-parent -5)
(smie-rule-parent)))
((and (smie-rule-parent-p "COMMA")
(not (smie-rule-hanging-p)))
(smie-rule-parent elixir-smie-indent-basic))
((smie-rule-parent-p "(")
(smie-rule-parent))
((smie-rule-parent-p "if")
(smie-rule-parent))
((smie-rule-parent-p "->")
(smie-rule-parent))))
(`(:after . "COMMA")
(cond
((and (smie-rule-parent-p ";")
(smie-rule-hanging-p))
(smie-rule-parent elixir-smie-indent-basic))
((and (smie-rule-parent-p "with")
(smie-rule-hanging-p))
(smie-rule-parent 5))
((and (smie-rule-parent-p "{")
(smie-rule-hanging-p))
(smie-rule-parent elixir-smie-indent-basic))
((and (smie-rule-parent-p "[")
(smie-rule-hanging-p))
0)
((smie-rule-parent-p "COMMA")
(smie-rule-parent elixir-smie-indent-basic))
((smie-rule-parent-p "->")
(smie-rule-parent elixir-smie-indent-basic))
(t (smie-rule-parent elixir-smie-indent-basic))))
(`(:before . "OP")
(cond
((smie-rule-parent-p "for")
(smie-rule-parent))
((and (not (smie-rule-hanging-p))
(elixir-smie-current-line-start-with-pipe-operator-p)
(elixir-smie-last-line-is-assignment-p))
(smie-rule-parent))
((and (not (smie-rule-hanging-p))
(elixir-smie-current-line-start-with-pipe-operator-p))
(cons 'column (elixir-smie--previous-line-indentation)))
((and (not (smie-rule-hanging-p))
(smie-rule-prev-p "OP"))
(- elixir-smie-indent-basic))
((smie-rule-parent-p "def" "defp" "defmacro" "defmacrop")
(smie-rule-parent))
(t (smie-rule-parent))))
(`(:after . "OP")
(cond
((smie-rule-sibling-p) nil)
((smie-rule-hanging-p)
(smie-rule-parent elixir-smie-indent-basic))
((and (not (smie-rule-sibling-p))
(not (smie-rule-hanging-p))
(smie-rule-parent-p "do:"))
(smie-rule-parent))
((smie-rule-parent-p ";")
(smie-rule-parent))
(t (smie-rule-parent (- elixir-smie-indent-basic)))))
(`(:before . "MATCH-STATEMENT-DELIMITER")
(cond
((and (smie-rule-parent-p "do")
(smie-rule-hanging-p))
(smie-rule-parent))
((and (smie-rule-parent-p "do")
(not (smie-rule-hanging-p)))
(smie-rule-parent elixir-smie-indent-basic))
((and (smie-rule-parent-p "fn"))
(smie-rule-parent elixir-smie-indent-basic))
;; There is a case when between two line inside a def block
;; when jumping to the next line and indent, where the cursor
;; jumps too much in front.
;;
;; Example:
;; def generate_pkg(path, opts) do
;; name = Path.basename(Path.expand(path))
;;
;; File.mkdir_p!(path)
;; <-
;; File.cd! path, fn ->
;; _generate_pkg(name, opts)
;; end
;; end
((and (smie-rule-parent-p "do")
(not (smie-rule-hanging-p)))
0)
((and (not (smie-rule-sibling-p))
(nth 2 smie--parent)
(smie-rule-hanging-p))
(smie-rule-parent elixir-smie-indent-basic))
((and (not (smie-rule-sibling-p))
(not (nth 2 smie--parent))
(smie-rule-hanging-p))
(smie-rule-parent))))
(`(:after . "MATCH-STATEMENT-DELIMITER")
(cond
((and (smie-rule-parent-p "MATCH-STATEMENT-DELIMITER")
(smie-rule-hanging-p)
(smie-rule-sibling-p))
(smie-rule-parent))
((and (smie-rule-parent-p "after")
(smie-rule-hanging-p))
(smie-rule-parent elixir-smie-indent-basic))
;; Correct indentation after a one-line fn definition
;; Example:
;;
;; sum = Enum.reduce(dbms, fn(x, sum) -> x + sum end)
;; average_dbm = sum / length(addresses)
((smie-rule-parent-p "fn")
(smie-rule-parent elixir-smie-indent-basic))
(t
(smie-rule-parent))))
(`(:before . "fn")
(cond
((smie-rule-parent-p "(")
(smie-rule-parent))
(t (smie-rule-parent))))
(`(:before . "for")
(cond
((elixir-smie-last-line-end-with-block-operator-p)
(smie-rule-parent elixir-smie-indent-basic))
((and (smie-rule-parent-p ";")
(smie-rule-prev-p "OP"))
(smie-rule-parent))
((smie-rule-prev-p "OP" "def")
(smie-rule-parent -2))))
(`(:before . "into:")
(cond
((smie-rule-parent-p "COMMA")
(smie-rule-parent elixir-smie-indent-basic))))
(`(:before . "do:")
(cond
((smie-rule-parent-p "def" "defp" "defmacro" "defmacrop")
(if (save-excursion
(move-beginning-of-line 1)
(looking-at "^\s*do:.+$"))
(smie-rule-parent)
(smie-rule-parent)))
;; Example
;;
;; hi = for i <- list, do: i
;; # weird spacing now <- Indent
;;
;; for i <- list, do: i
;; IO.puts 'WORKED' <- Indent
((and (smie-rule-parent-p "for")
(not (smie-rule-hanging-p)))
(smie-rule-parent))
((and (smie-rule-parent-p ";")
(not (smie-rule-hanging-p))
(save-excursion
(move-beginning-of-line 1)
(looking-at "^\s*do:.+$")))
(if (> (nth 0 (syntax-ppss)) 0)
(smie-rule-parent (- 3))
(smie-rule-parent elixir-smie-indent-basic)))
((and (smie-rule-parent-p ";")
(not (smie-rule-hanging-p)))
(if (> (nth 0 (syntax-ppss)) 0)
(smie-rule-parent (- elixir-smie-indent-basic))
(smie-rule-parent)))
((and (smie-rule-parent-p "OP")
(not (smie-rule-hanging-p)))
(smie-rule-parent elixir-smie-indent-basic))
((and (smie-rule-parent-p "COMMA")
(not (smie-rule-hanging-p)))
(smie-rule-parent elixir-smie-indent-basic))))
(`(:before . "do")
(cond
((and (smie-rule-parent-p "for")
(smie-rule-hanging-p))
(if (save-excursion
(move-beginning-of-line 1)
(looking-at "^.+\sfor\s.+\sdo\s*"))
(smie-rule-parent elixir-smie-indent-basic)
(smie-rule-parent (+ elixir-smie-indent-basic
elixir-smie-indent-basic))))
((and (smie-rule-parent-p "case")
(smie-rule-hanging-p))
(smie-rule-parent elixir-smie-indent-basic))
;; There is a case when between two line inside a def block
;; when jumping to the next line and indent, where the cursor
;; jumps too much in front.
;;
;; Example:
;; def generate_pkg(path, opts) do
;; name = Path.basename(Path.expand(path))
;;
;; File.mkdir_p!(path)
;; <-
;; File.cd! path, fn ->
;; _generate_pkg(name, opts)
;; end
;; end
((and (smie-rule-parent-p "def")
(smie-rule-hanging-p))
(smie-rule-parent elixir-smie-indent-basic))
(t elixir-smie-indent-basic)))
(`(:before . "end")
(cond
((smie-rule-parent-p "for")
(smie-rule-parent))
((smie-rule-parent-p "(")
(smie-rule-parent))
(t (smie-rule-parent))))
(`(:before . "else:")
(cond
((smie-rule-parent-p ";")
(if (> (nth 0 (syntax-ppss)) 0)
(smie-rule-parent elixir-smie-indent-basic)
(smie-rule-parent)))
((smie-rule-parent-p "if")
(smie-rule-parent elixir-smie-indent-basic))
(t (smie-rule-parent))))
;; Closing paren on the other line
(`(:before . "(")
(cond
((smie-rule-parent-p "fn")
(smie-rule-parent elixir-smie-indent-basic))
;; Indent parenthesis correctly inside a block
;;
;; Example:
;;
;; def bar do
;; ()
;; .....
((smie-rule-parent-p "do")
(smie-rule-parent))
((smie-rule-parent-p "OP")
(smie-rule-parent))
((and (smie-rule-parent-p "with")
(smie-rule-hanging-p))
(smie-rule-parent))
((and (smie-rule-parent-p "with")
(not (smie-rule-hanging-p)))
(smie-rule-parent 3))
((smie-rule-parent-p ";")
(smie-rule-parent))
(t (smie-rule-parent))))
(`(:before . "[")
(cond
((smie-rule-hanging-p)
(smie-rule-parent))))
(`(:before . "{")
(cond
((smie-rule-parent-p "COMMA")
(smie-rule-parent))
;; Example
;;
;; case parse do
;; { [ help: true ], _, _ }
;; -> :help
;; { _, [ user, project, count ], _ }
((and (not (smie-rule-hanging-p))
(smie-rule-parent-p "do"))
;; If the last line ends with a block operator `->'
;; indent two spaces more
;;
;; Example
;;
;; case File.read("/usr/share/dict/words") do
;; {:ok, contents} ->
;; {:something, contents} <- Indent here two spaces
;; ...
(if (elixir-smie-last-line-end-with-block-operator-p)
(smie-rule-parent elixir-smie-indent-basic)))
((and (smie-rule-parent-p "MATCH-STATEMENT-DELIMITER")
(not (smie-rule-hanging-p)))
(if (elixir-smie-last-line-end-with-block-operator-p)
(smie-rule-parent elixir-smie-indent-basic)
(if (elixir-smie-last-line-start-with-block-operator-p)
(smie-rule-parent (- elixir-smie-indent-basic))
(smie-rule-parent))))
((and (smie-rule-parent-p "OP")
(smie-rule-hanging-p))
(smie-rule-parent))
((smie-rule-parent-p ";")
(if (save-excursion
(move-end-of-line 1)
(looking-back elixir-smie--block-operator-regexp (- (point) 3) t))
(smie-rule-parent (- elixir-smie-indent-basic))
(if (save-excursion
(move-beginning-of-line 1)
(looking-at "^.+->.+$"))
(smie-rule-parent (- elixir-smie-indent-basic))
(smie-rule-parent))))))
(`(:after . "{")
(cond
((smie-rule-hanging-p)
(smie-rule-parent elixir-smie-indent-basic))
(t elixir-smie-indent-basic)))
(`(:after . "[")
(cond
((smie-rule-hanging-p)
(smie-rule-parent elixir-smie-indent-basic))
(t elixir-smie-indent-basic)))
(`(:before . "if")
(cond
;; Indent when if is inside a `->' block
;;
;; Example:
;;
;; whatever ->
;; if true do <-
;; :foo
;; end
;; ....
((elixir-smie-last-line-end-with-block-operator-p)
(smie-rule-parent elixir-smie-indent-basic))
;; Indent if inside else
;;
;; Example:
;;
;; else
;; if condition, do: :bar <-
;; end
((smie-rule-parent-p "else")
(smie-rule-parent elixir-smie-indent-basic))
(t (smie-rule-parent))))
(`(:before . "->")
(cond
;; Example
;;
;; receive do
;; after
;; 2000 ->
;; IO.puts 'hello'
;; IO.puts 'status 2000 ends' <- Indent second line
;; { :ok } ->
;; ....
((and (smie-rule-parent-p "after")
(not (smie-rule-sibling-p)))
(smie-rule-parent (+ elixir-smie-indent-basic
elixir-smie-indent-basic)))
;; Example
;;
;; case parse do
;; { [ help: true ], _, _ }
;; -> :help
;; ...
((and (not (smie-rule-hanging-p))
(smie-rule-parent-p "do"))
elixir-smie-indent-basic)
((and (not (smie-rule-hanging-p))
(smie-rule-parent-p "MATCH-STATEMENT-DELIMITER"))
(smie-rule-parent))
(t (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 "catch" "rescue" "else")
(smie-rule-parent (+ elixir-smie-indent-basic
elixir-smie-indent-basic)))
((smie-rule-parent-p "do" "try")
(smie-rule-parent elixir-smie-indent-basic))
;; Example
;;
;; receive do
;; after
;; 2000 ->
;; IO.puts 'hello' <- Indent two spaces
((and (smie-rule-parent-p "after")
(smie-rule-hanging-p)
(not (smie-rule-sibling-p)))
(smie-rule-parent (+ elixir-smie-indent-basic
elixir-smie-indent-basic)))
(t (smie-rule-parent elixir-smie-indent-basic))))))
(`(:before . ";")
(cond
;; Handle cases where built in keywords are used
;; as function names.
;;
;; Example:
;;
;; def foo(test) do
;; test_case = test.case
;; run(test_case)
;; end
((and (smie-rule-parent-p "case" "try" "rescue")
(smie-rule-hanging-p)
(elixir-smie-current-line-contains-built-in-keyword-p))
(+ (- (cdr (smie-rule-parent))) (+ elixir-smie-indent-basic
elixir-smie-indent-basic)))
;; There is a case after an one line definition of functions/macros
;; when an `if' keyword token is involved, where the next block `end'
;; token will have a `if' as parent and it's hanging.
;;
;; Example:
;;
;; defmacro my_if(expr, do: if_block), do: if(expr, do: if_block, else: nil)
;; defmacro my_if(expr, do: if_block, else: else_block) do
;; ...
;; end <- parent is `if`
((and (smie-rule-parent-p "if")
(smie-rule-hanging-p))
(smie-rule-parent))
((and (smie-rule-parent-p "else")
(smie-rule-hanging-p))
(smie-rule-parent elixir-smie-indent-basic))
((smie-rule-parent-p "catch" "def" "defmodule" "defp" "do" "else"
"fn" "if" "rescue" "try" "unless" "defmacro" "defmacrop")
(smie-rule-parent))
((smie-rule-parent-p "after")
(smie-rule-parent elixir-smie-indent-basic))
;; Example
;;
;; case parse do
;; { [ help: true ], _, _ }
;; -> :help
;; { _, [ user, project, count ], _ }
;; -> { user, project, count }
;; ...
((and (smie-rule-parent-p "->")
(smie-rule-hanging-p))
(smie-rule-parent))
((and (smie-rule-parent-p ";")
(smie-rule-hanging-p)
(save-excursion
(move-beginning-of-line 1)
(looking-at "^\s+else:.+$"))
(not (save-excursion
(move-beginning-of-line 1)
(looking-at "^\s+else:.+)$"))))
(smie-rule-parent (- elixir-smie-indent-basic)))
((and (smie-rule-parent-p ";")
(save-excursion
(move-beginning-of-line 1)
(looking-at "^.+,$")))
(smie-rule-parent))
((and (smie-rule-parent-p ";")
(smie-rule-hanging-p)
(save-excursion
(move-beginning-of-line 1)
(looking-at "^\s+do:.+$"))
(not (save-excursion
(move-beginning-of-line 1)
(looking-at "^\s+do:.+)$"))))
(smie-rule-parent))
((elixir-smie-current-line-start-with-pipe-operator-p)
(smie-rule-parent))
((smie-rule-parent-p "(")
(smie-rule-parent elixir-smie-indent-basic))))
(`(:after . ";")
(cond
((smie-rule-parent-p "def")
(smie-rule-parent))
((and (smie-rule-parent-p "if")
(elixir-smie-current-line-contains-built-in-keyword-p))
(+ (- (cdr (smie-rule-parent))) (+ elixir-smie-indent-basic
elixir-smie-indent-basic)))
((smie-rule-parent-p "if")
(smie-rule-parent))
((smie-rule-parent-p "after")
(smie-rule-parent elixir-smie-indent-basic))
((and (smie-rule-parent-p "(")
(boundp 'smie--parent)
(save-excursion
(goto-char (cadr smie--parent))
(smie-rule-hanging-p)))
(smie-rule-parent elixir-smie-indent-basic))))))
(defun elixir-smie--heredoc-at-current-point-p ()
"Return non-nil if cursor is at a string."
(save-excursion
(or (and (nth 3 (save-excursion
(let ((pos (point)))
(parse-partial-sexp 1 pos))))
(nth 8 (save-excursion
(let ((pos (point)))
(parse-partial-sexp 1 pos)))))
(and (looking-at "\"\"\"")
(match-beginning 0)))))
(defun elixir-smie--previous-line-empty-p ()
"Return non-nil if the previous line is blank."
(save-excursion
(forward-line -1)
(move-beginning-of-line 1)
(looking-at "[[:space:]]*$")))
(defun elixir-smie--previous-line-indentation ()
"Return the indentation of the previous line."
(save-excursion
(forward-line -1)
(current-indentation)))
;; Add the custom function to handle indentation inside heredoc to the
;; smie-indent-functions list. The indentation function will only be
;; process inside an elixir-mode.
(defun elixir-smie--indent-inside-heredoc ()
"Handle indentation inside Elixir heredocs.
Rules:
1. If the previous line is empty, indent as the basic indentation
at the beginning of the heredoc.
2. If the previous line is not empty, indent as the previous line.
"
(if (eq major-mode 'elixir-mode)
(if (elixir-smie--heredoc-at-current-point-p)
(let ((indent
(save-excursion
(when (re-search-backward "^\\(\s+\\)\\(@doc\\|@moduledoc\\|.*\\)\"\"\"" nil t)
(string-width (match-string 1))))))
(cond
((elixir-smie--previous-line-empty-p)
(goto-char indent))
((and (not (save-excursion (looking-at "\"\"\"")))
(not (elixir-smie--previous-line-empty-p)))
(goto-char (elixir-smie--previous-line-indentation)))
(indent
(goto-char indent)))))))
(defun elixir-smie-empty-string-p (string)
"Return non-nil if STRING is null, blank or whitespace only."
(or (null string)
(string= string "")
(if (string-match-p "^\s+$" string) t)))
(add-to-list 'smie-indent-functions 'elixir-smie--indent-inside-heredoc)
(provide 'elixir-smie)
;;; elixir-smie.el ends here