diff --git a/smartparens-elixir.el b/smartparens-elixir.el index 8411162c..820c8df1 100644 --- a/smartparens-elixir.el +++ b/smartparens-elixir.el @@ -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. @@ -102,32 +143,70 @@ ID, ACTION, CONTEXT." (sp-local-pair "def" "end" :when '(("SPC" "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" "")) :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" "")) :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" "")) + :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" "")) :post-handlers '("| ")) (sp-local-pair "if" "end" + :when '(("SPC" "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" "")) + :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" "")) + :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" "")) :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" "")) :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" "")) :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" "")) + :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" "")) + :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" "")) + :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" "")) + :skip-match 'sp-elixir-skip-keyword-list-def-p :post-handlers '(sp-elixir-empty-do-block-post-handler)) ) diff --git a/test/smartparens-elixir-test.el b/test/smartparens-elixir-test.el index 082fbcd9..7494e862 100644 --- a/test/smartparens-elixir-test.el +++ b/test/smartparens-elixir-test.el @@ -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 |