Skip to content

Commit

Permalink
fix Fuco1#707
Browse files Browse the repository at this point in the history
  • Loading branch information
andreyorst committed Dec 23, 2020
1 parent b8bdd92 commit da3a4eb
Show file tree
Hide file tree
Showing 2 changed files with 192 additions and 28 deletions.
135 changes: 107 additions & 28 deletions smartparens-elixir.el
Expand Up @@ -30,37 +30,78 @@
(--each '(elixir-mode)
(add-to-list 'sp-sexp-suffix (list it 'regexp "")))

(defun sp-elixir-def-p (id)
"Return non-nil if the \"do\" keyword is part of definition.
ID is the opening delimiter.
Definitions are the constructions of the form defmodule-do-end,
def-do-end and similar pairs."
(save-excursion
(when (equal "do" id)
(back-to-indentation)
(looking-at (regexp-opt '(
"defmodule"
"defmacro"
"defmacrop"
"quote"
"def"
"defp"
"if"
"unless"
"case"
"cond"
"with"
"for"
"receive"
"try"
))))))
(defvar sp-elixir-builtins
(regexp-opt '("defmodule" "defmacro" "defmacrop" "def" "defp" "defimpl"
"if" "unless" "case" "cond"
"with" "for" "receive" "try" "quote")
'words)
"Regexp that matches opening delimiters for definitions.
Definitions require either comma followed by \"do:\" keyword
list, or \"do\" \"end\" block delimiters.")

(defun sp-elixir-skip-do-keyword-p-fun (bodyless-ms keywords)
"Define a function that will test if any of keywords is part of definition.
BODYLESS-MS is an keyword that supports bodyless form, like
\"def\" or \"defp\". KEYWORDS is additional regexp for keywords
to check in order to skip definition.
This line-based search terminates early if any of
`sp-elixir-builtins' were found."
(lambda (ms _mb _me)
(unless (or (equal "end" ms) (equal "do" ms))
(or (string-match-p keywords (thing-at-point 'line t))
(catch 'definition
(save-excursion
(while t
(forward-line 1)
(let ((line (string-trim-left (thing-at-point 'line t))))
(unless (string-match-p "\\s-*#" line) ;; skip full line comments
(cond ((eq (string-match-p "\\bend\\b" line) 0)
(throw 'definition nil))
;; if BODYLESS-MS was supplied, means we're trying
;; to match bodyless form
((and bodyless-ms (eq (string-match-p bodyless-ms line) 0))
(throw 'definition t))
;; Terminate the search if we find any of
;; `sp-elixir-builtins' as we're usually
;; searching for "end"
((eq (string-match-p sp-elixir-builtins line) 0)
(throw 'definition nil))
;; "do:" keyword means that there will be no
;; "end" so we skip this definition
((eq (string-match-p keywords line) 0)
(throw 'definition t))
((eobp) (throw 'definition nil))))))))))))

;; Special functions for SKIP-MATCH parameter of `sp-pair'

(fset 'sp-elixir-skip-keyword-list-def-p (sp-elixir-skip-do-keyword-p-fun nil "\\bdo:"))
(fset 'sp-elixir-skip-bodyless-def-p (sp-elixir-skip-do-keyword-p-fun "\\bdef\\b" "\\bdo:"))
(fset 'sp-elixir-skip-bodyless-defp-p (sp-elixir-skip-do-keyword-p-fun "\\bdefp\\b" "\\bdo:"))
(fset 'sp-elixir-skip-for-in-defimpl-p (sp-elixir-skip-do-keyword-p-fun nil "\\b\\(defimpl\\b\\|do:\\)"))

(defun sp-elixir-skip-def-p (ms _mb _me)
"Test if \"do\" is part of definition.
MS, MB, ME."
(sp-elixir-def-p ms))
MS must be \"do\" keyword.
Definitions in Elixir can contain any of `sp-elixir-builtins'
followed with \"do\" keyword and closed with \"end\" keyword,
which may not be on the same line."
(when (equal "do" ms)
(save-excursion
(catch 'definition
(while t
(let ((line (string-trim-left (thing-at-point 'line t))))
(unless (string-match-p "\\s-*#" line)
(cond ((eq (string-match-p sp-elixir-builtins line) 0)
(throw 'definition t))
((eq (string-match-p "\\bend\\b" line) 0)
(throw 'definition nil))
((bobp) (throw 'definition nil)))))
(forward-line -1))))))

(defun sp-elixir-do-block-post-handler (_id action _context)
"Insert \"do\" keyword and indent the new block.
Expand Down Expand Up @@ -102,32 +143,70 @@ ID, ACTION, CONTEXT."
(sp-local-pair "def" "end"
:when '(("SPC" "RET" "<evil-ret>"))
:post-handlers '(sp-elixir-do-block-post-handler)
:skip-match 'sp-elixir-skip-bodyless-def-p
:unless '(sp-in-comment-p sp-in-string-p))
(sp-local-pair "defp" "end"
:when '(("SPC" "RET" "<evil-ret>"))
:post-handlers '(sp-elixir-do-block-post-handler)
:skip-match 'sp-elixir-skip-bodyless-defp-p
:unless '(sp-in-comment-p sp-in-string-p))
(sp-local-pair "defmodule" "end"
:when '(("SPC" "RET" "<evil-ret>"))
:post-handlers '(sp-elixir-do-block-post-handler)
:skip-match 'sp-elixir-skip-keyword-list-def-p
:unless '(sp-in-comment-p sp-in-string-p))
(sp-local-pair "defimpl" "end"
:when '(("SPC" "RET" "<evil-ret>"))
:post-handlers '(sp-elixir-do-block-post-handler)
:skip-match 'sp-elixir-skip-keyword-list-def-p
:unless '(sp-in-comment-p sp-in-string-p))
(sp-local-pair "fn" "end"
:when '(("SPC" "RET" "<evil-ret>"))
:post-handlers '("| "))
(sp-local-pair "if" "end"
:when '(("SPC" "RET" "<evil-ret>"))
:post-handlers '(sp-elixir-do-block-post-handler)
:skip-match 'sp-elixir-skip-keyword-list-def-p
:unless '(sp-in-comment-p sp-in-string-p))
(sp-local-pair "for" "end"
:when '(("SPC" "RET" "<evil-ret>"))
:post-handlers '(sp-elixir-do-block-post-handler)
:skip-match 'sp-elixir-skip-for-in-defimpl-p
:unless '(sp-in-comment-p sp-in-string-p))
(sp-local-pair "cond" "end"
:when '(("SPC" "RET" "<evil-ret>"))
:post-handlers '(sp-elixir-do-block-post-handler)
:unless '(sp-in-comment-p sp-in-string-p))
(sp-local-pair "with" "end"
:when '(("SPC" "RET" "<evil-ret>"))
:post-handlers '(sp-elixir-do-block-post-handler)
:unless '(sp-in-comment-p sp-in-string-p))
(sp-local-pair "unless" "end"
:when '(("SPC" "RET" "<evil-ret>"))
:post-handlers '(sp-elixir-do-block-post-handler)
:skip-match 'sp-elixir-skip-keyword-list-def-p
:unless '(sp-in-comment-p sp-in-string-p))
(sp-local-pair "case" "end"
:when '(("SPC" "RET" "<evil-ret>"))
:post-handlers '(sp-elixir-do-block-post-handler)
:skip-match 'sp-elixir-skip-keyword-list-def-p
:unless '(sp-in-comment-p sp-in-string-p))
(sp-local-pair "try" "end"
:when '(("SPC" "RET" "<evil-ret>"))
:post-handlers '(sp-elixir-do-block-post-handler)
:skip-match 'sp-elixir-skip-keyword-list-def-p
:unless '(sp-in-comment-p sp-in-string-p))
(sp-local-pair "receive" "end"
:when '(("RET" "<evil-ret>"))
:skip-match 'sp-elixir-skip-keyword-list-def-p
:post-handlers '(sp-elixir-empty-do-block-post-handler))
(sp-local-pair "quote" "end"
:when '(("RET" "<evil-ret>"))
:skip-match 'sp-elixir-skip-keyword-list-def-p
:post-handlers '(sp-elixir-empty-do-block-post-handler))
(sp-local-pair "defmacro" "end"
:when '(("RET" "<evil-ret>"))
:skip-match 'sp-elixir-skip-keyword-list-def-p
:post-handlers '(sp-elixir-empty-do-block-post-handler))
)

Expand Down
85 changes: 85 additions & 0 deletions test/smartparens-elixir-test.el
Expand Up @@ -107,6 +107,91 @@ end"
end"
'(:beg 56 :end 97 :op "unless" :cl "end" :prefix "" :suffix "")))


(ert-deftest sp-test-elixir-parse-bodyless-def ()
"Parse bodyless def correctly"
(sp-test-elixir-parse "|defmodule HelloWorld do
def hello
def hello do
IO.puts \"Hello world\"
end
end"
'(:beg 1 :end 87 :op "defmodule" :cl "end" :prefix "" :suffix "")))

(ert-deftest sp-test-elixir-parse-bodyless-defp-w-keyword-list ()
"Parse bodyless defp correctly with keyword-list body"
(sp-test-elixir-parse "|defmodule HelloWorld do
def hello
def hello, do: IO.puts \"Hello world\"
end"
'(:beg 1 :end 79 :op "defmodule" :cl "end" :prefix "" :suffix "")))

(ert-deftest sp-test-elixir-parse-bodyless-defp ()
"Parse bodyless defp correctly"
(sp-test-elixir-parse "|defmodule HelloWorld do
defp hello
defp hello do
IO.puts \"Hello world\"
end
end"
'(:beg 1 :end 89 :op "defmodule" :cl "end" :prefix "" :suffix "")))

(ert-deftest sp-test-elixir-parse-bodyless-defp-w-keyword-list ()
"Parse bodyless defp correctly with keyword-list body"
(sp-test-elixir-parse "|defmodule HelloWorld do
defp hello
defp hello, do: IO.puts \"Hello world\"
end"
'(:beg 1 :end 81 :op "defmodule" :cl "end" :prefix "" :suffix "")))

(ert-deftest sp-test-elixir-parse-keyword-def ()
"Parse def with do: keyword correctly"
(sp-test-elixir-parse "|defmodule HelloWorld do
def hello, do: IO.puts \"Hello world\"
end"
'(:beg 1 :end 67 :op "defmodule" :cl "end" :prefix "" :suffix "")))

(ert-deftest sp-test-elixir-parse-multiline-keyword-def ()
"Parse multiline def with do: keyword correctly"
(sp-test-elixir-parse "|defmodule HelloWorld do
def hello,
do:
IO.puts \"Hello world\"
end"
'(:beg 1 :end 73 :op "defmodule" :cl "end" :prefix "" :suffix "")))

(ert-deftest sp-test-elixir-parse-keyword-defp ()
"Parse oneline defp with do: keyword correctly"
(sp-test-elixir-parse "|defmodule HelloWorld do
defp hello, do: IO.puts \"Hello world\"
end"
'(:beg 1 :end 68 :op "defmodule" :cl "end" :prefix "" :suffix "")))

(ert-deftest sp-test-elixir-parse-multiline-keyword-defp ()
"Parse multiline defp with do: keyword correctly"
(sp-test-elixir-parse "|defmodule HelloWorld do
defp hello,
do:
IO.puts \"Hello world\"
end"
'(:beg 1 :end 74 :op "defmodule" :cl "end" :prefix "" :suffix "")))

(ert-deftest sp-test-elixir-parse-defimpl-with-for-keyword ()
"Parse defimpl with for: keyword."
(sp-test-elixir-parse "|defimpl Type, for: OtherType do
for n <- [1, 2, 3], do: n * n
end"
'(:beg 1 :end 68 :op "defimpl" :cl "end" :prefix "" :suffix "")))

(ert-deftest sp-test-elixir-parse-with ()
"Parse with statement."
(sp-test-elixir-parse "|with {:ok, val} <- foo() do
val
else
:err
end"
'(:beg 1 :end 50 :op "with" :cl "end" :prefix "" :suffix "")))

(ert-deftest sp-test-elixir-receive-block-insertion ()
(sp-test-insertion-elixir "|" "receive" "receive do
|
Expand Down

0 comments on commit da3a4eb

Please sign in to comment.