diff --git a/elixir-smie.el b/elixir-smie.el index 0736e69c..2dca60f0 100644 --- a/elixir-smie.el +++ b/elixir-smie.el @@ -42,7 +42,7 @@ (statements (statement) (statement ";" statements)) (statement ("def" non-block-expr "do" statements "end") - (non-block-expr "fn" match-statement "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") @@ -76,9 +76,11 @@ (left "*" "/")))))) (defvar elixir-smie--operator-regexp - (regexp-opt '("<<<" ">>>" "^^^" "~~~" "&&&" "|||" "===" "!==" "==" "!=" "<=" - ">=" "<" ">" "&&" "||" "<>" "++" "--" "//" - "/>" "=~" "|>" "->"))) + (rx (or "<<<" ">>>" "^^^" "~~~" "&&&" "|||" "===" "!==" "==" "!=" "<=" + ">=" "<" ">" "&&" "||" "<>" "++" "--" "//" "/>" "=~" "|>"))) + +(defvar elixir-smie--block-operator-regexp + (rx "->")) (defvar elixir-smie--spaces-til-eol-regexp (rx (and (1+ space) eol)) @@ -96,6 +98,40 @@ (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." + (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. + (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 @@ -105,7 +141,12 @@ ";") ((and (looking-at "[\n#]") (elixir-smie--implicit-semi-p)) (if (eolp) (forward-char 1) (forward-comment 1)) - ";") + (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") @@ -117,7 +158,12 @@ (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--operator-regexp (- (point) 3) t) (goto-char (match-beginning 0)) "OP") @@ -140,6 +186,54 @@ ((smie-rule-sibling-p) nil) ((smie-rule-hanging-p) (smie-rule-parent elixir-smie-indent-basic)) (t elixir-smie-indent-basic))) + + (`(:before . "MATCH-STATEMENT-DELIMITER") + (cond + ((and (not (smie-rule-sibling-p)) + (smie-rule-hanging-p)) + (smie-rule-parent elixir-smie-indent-basic)))) + ;; ((and (smie-rule-hanging-p) + ;; (smie-rule-sibling-p)) + ;; (smie-rule-parent)))) + + (`(:after . "MATCH-STATEMENT-DELIMITER") + (cond + ;; We don't want to specify any rules for the first `->' after `do' or + ;; `fn', since SMIE will look at the BNF to see how to handle indentation + ;; in that case. + ((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))) + ;; Otherwise, if just indent by two. + ((smie-rule-hanging-p) + elixir-smie-indent-basic))) + (`(:before . ";") (cond ((smie-rule-parent-p "after" "catch" "def" "defmodule" "defp" "do" "else" @@ -147,7 +241,7 @@ (smie-rule-parent elixir-smie-indent-basic)))) (`(:after . ";") (if (smie-rule-parent-p "if") - (smie-rule-parent 0))))) + (smie-rule-parent))))) (define-minor-mode elixir-smie-mode "SMIE-based indentation and syntax for Elixir" diff --git a/test/elixir-mode-indentation-tests.el b/test/elixir-mode-indentation-tests.el index c077a03a..8dad3af6 100644 --- a/test/elixir-mode-indentation-tests.el +++ b/test/elixir-mode-indentation-tests.el @@ -15,7 +15,7 @@ ;; indeed broken. My intention is that while working on a specific problem, ;; the failure expectation will be removed so that we know when the test case ;; passes. -(elixir-def-indentation-test indents-use-dot-module-newline () +(elixir-def-indentation-test indent-use-dot-module-newline () "defmodule Foo do use GenServer.Behaviour @@ -31,7 +31,7 @@ end" end end") -(elixir-def-indentation-test indents-use-dot-module () +(elixir-def-indentation-test indent-use-dot-module () " defmodule Foo do use GenServer.Behaviour @@ -47,7 +47,7 @@ defmodule Foo do end end") -(elixir-def-indentation-test indents-do-blocks () +(elixir-def-indentation-test indent-do-blocks () " defmodule Foo do def foobar do @@ -63,7 +63,7 @@ defmodule Foo do end end") -(elixir-def-indentation-test indents-do-blocks-after-linebreak-two () +(elixir-def-indentation-test indent-do-blocks-after-linebreak-two () " defmodule FooBar do def foo do @@ -89,7 +89,7 @@ defmodule FooBar do end end") -(elixir-def-indentation-test indents-do-blocks-after-linebreak-three () +(elixir-def-indentation-test indent-do-blocks-after-linebreak-three () " defmodule FooBar do def foo do @@ -125,7 +125,7 @@ defmodule FooBar do end end") -(elixir-def-indentation-test indents-do-blocks-with-space-after-inline () +(elixir-def-indentation-test indent-do-blocks-with-space-after-inline () "defmodule Foo do def foobar do if true, do: IO.puts \"yay\" @@ -141,7 +141,7 @@ end" end end") -(elixir-def-indentation-test indents-after-empty-line +(elixir-def-indentation-test indent-after-empty-line () " def foo do @@ -160,7 +160,7 @@ def foo do c = a * b end") -(elixir-def-indentation-test indents-function-calls-without-parens () +(elixir-def-indentation-test indent-function-calls-without-parens () " test \"foo\" do assert true, \"should be true\" @@ -174,7 +174,7 @@ test \"foo\" do end ") -(elixir-def-indentation-test indents-records-correctly () +(elixir-def-indentation-test indent-records-correctly () " defmodule MyModule do require Record @@ -192,7 +192,7 @@ defmodule MyModule do end ") -(elixir-def-indentation-test indents-continuation-lines () +(elixir-def-indentation-test indent-continuation-lines () " def foo do has_something(x) && @@ -208,7 +208,7 @@ def foo do end ") -(elixir-def-indentation-test indents-continuation-lines-with-comments/1 +(elixir-def-indentation-test indent-continuation-lines-with-comments/1 () " has_something(x) && # foo @@ -221,7 +221,7 @@ has_something(x) && # foo has_something(z) ") -(elixir-def-indentation-test indents-continuation-lines-with-comments/2 () +(elixir-def-indentation-test indent-continuation-lines-with-comments/2 () " has_something(x) && has_something(y) || # foo @@ -233,7 +233,7 @@ has_something(x) && has_something(z) ") -(elixir-def-indentation-test indents-last-commented-line +(elixir-def-indentation-test indent-last-commented-line () " defmodule Foo do @@ -254,7 +254,7 @@ defmodule Foo do end ") -(elixir-def-indentation-test indents-if () +(elixir-def-indentation-test indent-if () " if condition do yes @@ -264,7 +264,7 @@ if condition do yes end") -(elixir-def-indentation-test indents-if-else () +(elixir-def-indentation-test indent-if-else () " if condition do yes @@ -278,7 +278,7 @@ else no end") -(elixir-def-indentation-test indents-try () +(elixir-def-indentation-test indent-try () " try do foo @@ -290,7 +290,7 @@ try do bar end") -(elixir-def-indentation-test indents-try/after () +(elixir-def-indentation-test indent-try/after () " try do foo @@ -308,7 +308,7 @@ after post_that() end") -(elixir-def-indentation-test indents-try/catch/after () +(elixir-def-indentation-test indent-try/catch/after () " try do foo @@ -336,7 +336,7 @@ after post_that() end") -(elixir-def-indentation-test indents-fn-in-assignment () +(elixir-def-indentation-test indent-fn-in-assignment () " f = fn x, y -> x + y @@ -346,7 +346,7 @@ f = fn x, y -> x + y end") -(elixir-def-indentation-test indents-fn-as-arguments () +(elixir-def-indentation-test indent-fn-as-arguments () " Enum.map 1..10, fn x -> x + 1 @@ -356,7 +356,7 @@ Enum.map 1..10, fn x -> x + 1 end") -(elixir-def-indentation-test indents-list-argument-continuation-lines-nicely () +(elixir-def-indentation-test indent-list-argument-continuation-lines-nicely () " to_process = [27, 33, 35, 11, 36, 29, 18, 37, 21, 31, 19, 10, 14, 30, 15, 17, 23, 28, 25, 34, 22, 20, 13, 16, 32, 12, 26, 24] @@ -379,7 +379,7 @@ end" end end") -(elixir-def-indentation-test indents-list-of-floats-aligns +(elixir-def-indentation-test indent-list-of-floats-aligns () " [1.2, @@ -515,6 +515,58 @@ def foo do #comment :bar end") +(elixir-def-indentation-test indent-multiline-match () + " +def foo do + case is_string(x) do + true -> + x2 = \" one\" + x <> x2 + false -> + x2 = \" two\" + x <> x2 + end +end" + " +def foo do + case is_string(x) do + true -> + x2 = \" one\" + x <> x2 + false -> + x2 = \" two\" + x <> x2 + end +end" + ) + +(elixir-def-indentation-test indent-multiline-match/2 () + " +def foo do + case is_string(x) do + true -> + x2 = \" one\" + x <> x2 + + false -> + x2 = \" two\" + x <> x2 + end +end" + " +def foo do + case is_string(x) do + true -> + x2 = \" one\" + x <> x2 + + false -> + x2 = \" two\" + x <> x2 + end +end" + ) + (elixir-def-indentation-test indent-after-require-Record () ;; Mind the significant whitespace after `Record' in each case. There should