diff --git a/elixir-mode-tests.el b/elixir-mode-tests.el index e8ba7a8a..b3a63782 100644 --- a/elixir-mode-tests.el +++ b/elixir-mode-tests.el @@ -21,7 +21,6 @@ ,@body)) (load "test/elixir-mode-indentation-tests.el") -(load "test/elixir-mode-tokenizer-hl-tests.el") (load "test/elixir-mode-font-tests.el") (provide 'elixir-mode-tests) diff --git a/elixir-smie.el b/elixir-smie.el index 31fa7ca4..2c414e51 100644 --- a/elixir-smie.el +++ b/elixir-smie.el @@ -34,215 +34,90 @@ 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-statement "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 "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 + (regexp-opt '("<<<" ">>>" "^^^" "~~~" "&&&" "|||" "===" "!==" "==" "!=" "<=" + ">=" "<" ">" "&&" "||" "<>" "++" "--" "//" + "/>" "=~" "|>" "->"))) + +(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)) -(progn - (setq elixir-syntax-class-names nil) - - (defmacro elixir-smie-define-regexp-opt (name &rest table) - `(elixir-smie-define-regexp ,name (regexp-opt (list ,@table)))) - - (defmacro elixir-smie-define-regexp (name regexp &optional flag) - (let ((regex-name (intern (format "elixir-smie-%s" name)))) - `(progn - (defconst ,regex-name - ,regexp) - (pushnew `(,',regex-name . ,(upcase (symbol-name ',name))) elixir-syntax-class-names)))) - - (elixir-smie-define-regexp-opt op "&&" "||" "!") - (elixir-smie-define-regexp dot "\\.") - (elixir-smie-define-regexp comma ",") - (elixir-smie-define-regexp -> "->") - (elixir-smie-define-regexp << "<<") - (elixir-smie-define-regexp >> ">>") - (elixir-smie-define-regexp-opt parens "(" ")" "{" "}" "[" "]" "<<" ">>")) - -(defconst elixir-smie-block-intro-keywords - '(do else catch after rescue -> OP) - "Keywords in which newlines cause confusion for the parser.") - -(defun elixir-skip-comment-backward () - "Skip backwards over all whitespace and comments. - -Return non-nil if any line breaks were skipped." - (let ((start-line-no (line-number-at-pos (point)))) - (forward-comment (- (point))) - (/= start-line-no (line-number-at-pos (point))))) - -(defun elixir-skip-comment-forward () - "Skip forward over any whitespace and comments. - -Return non-nil if any line breaks were skipped." - (let ((start-line-no (line-number-at-pos (point)))) - (forward-comment (buffer-size)) - (/= start-line-no (line-number-at-pos (point))))) - -(defun elixir-smie-next-token-no-lookaround (forwardp) - (block elixir-smie-next-token-no-lookaround - ;; First, skip comments; but if any comments / newlines were - ;; skipped, the upper level needs to check if they were significant: - (when (if forwardp - (elixir-skip-comment-forward) - (elixir-skip-comment-backward)) - (return-from elixir-smie-next-token-no-lookaround "\n")) - (let* ((found-token-class (find-if - (lambda (class-def) - (let ((regex (symbol-value (car class-def)))) - (if forwardp - (looking-at regex) - (looking-back regex nil t)))) - elixir-syntax-class-names)) - (maybe-token - (let ((current-char (if forwardp - (following-char) - (preceding-char)))) - (cond ((member current-char - '(?\n ?\;)) - (if forwardp - (forward-comment (point-max)) - (forward-comment (- (point)))) - (string current-char)) - (found-token-class - (goto-char (if forwardp - (match-end 0) - (match-beginning 0))) - (if (string= "PARENS" (cdr found-token-class)) - (buffer-substring-no-properties (match-beginning 0) (match-end 0)) - (cdr found-token-class))) - ((when (= ?\" (char-syntax (if forwardp - (following-char) - (preceding-char)))) - (if forwardp - (forward-sexp) - (backward-sexp)) - "STRING")))))) - (or maybe-token - (downcase - (buffer-substring-no-properties - (point) - (if forwardp - (progn (skip-syntax-forward "'w_") - (point)) - (progn (skip-syntax-backward "'w_") - (point))))))))) +(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-next-token (forwardp) - (block elixir-smie-next-token - (let ((current-token (elixir-smie-next-token-no-lookaround forwardp))) - (when (string= "\n" current-token) - ;; This is a newline; if the previous token isn't an OP2, this - ;; means the line end marks the end of a statement & we get to - ;; scan forward until there's a non-newline token; otherwise, - ;; make this line ending something that probably ends the - ;; statement (but see below). - (if (save-excursion - (block nil - (let ((token (elixir-smie-next-token-no-lookaround nil))) - (while (and (not (= (point) (point-min))) - (string= "\n" token)) - (setq token (elixir-smie-next-token-no-lookaround nil))) - (when (member (intern token) elixir-smie-block-intro-keywords) - (return t))))) - ;; it's a continuation line, return the next token after the newline: - (return-from elixir-smie-next-token (elixir-smie-next-token forwardp)) - (setq current-token ";"))) - - ;; When reading match statements (the ones with expr -> statements), - ;; we need to drop non-; delimiters so the parser knows when a - ;; match statement ends and another begins, so scan around point to - ;; see if there are any -> within the current block's scope. - - ;; If the current token is a ";", scan forward to see if the current - ;; potential statement contains a "->". If so, scan back to find a - ;; "do". If there is a -> there, emit a match-statement-delimiter - ;; instead of the ";". - (if (and (string= ";" current-token) - ;; Scan ahead: - (let ((level 0) - token) - (save-excursion - (block nil - (while - (and - ;; Cursor is not at the end of the buffer... - (not (= (point) (point-max))) - ;; ...and the current token is not an empty string... - (not (string= "" token)) - ;; ...nor a newline nor a semicolon. - (not (or (string= "\n" token) (string= ";" token)))) - (setq token (elixir-smie-next-token-no-lookaround t)) - ;; If we're at the top level and the token is "->", - ;; return t - (cond ((and (= level 0) (string= "->" token)) - (return t)) - ;; If token is "do" or "fn", increment level - ((find token '("do" "fn") :test 'string=) - (incf level)) - ;; If token is "end", decrement level - ((string= token "end") - (decf level))))))) - ;; Scan behind: - (let (token) - (save-excursion - (block nil - (while - (and - ;; Cursor is not at the beginning of buffer... - (not (= (point) (point-min))) - ;; ...and token is neither empty string, nor "do"/"fn" - (not (string= "" token)) - (not (string= "do" token)) - (not (string= "fn" token))) - (setq token (elixir-smie-next-token-no-lookaround nil)) - (when (string= "->" token) - (return t))) - (when (string= token "do") t))))) - "MATCH-STATEMENT-DELIMITER" - current-token)))) +(defun elixir-smie--implicit-semi-p () + (not (or (memq (char-before) '(?\{ ?\[)) + (looking-back elixir-smie--operator-regexp (- (point) 3) t)))) (defun elixir-smie-forward-token () - (elixir-smie-next-token t)) + (cond + ((and (looking-at "[\n#]") (elixir-smie--implicit-semi-p)) + (if (eolp) (forward-char 1) (forward-comment 1)) + ";") + ((looking-at elixir-smie--operator-regexp) + (goto-char (match-end 0)) + "OP") + (t (smie-default-forward-token)))) (defun elixir-smie-backward-token () - (elixir-smie-next-token nil)) - -(defconst elixir-smie-grammar - (smie-prec2->grammar - (smie-bnf->prec2 - '((id) - (statements (statement) - (statement ";" statements)) - (statement ("def" non-block-expr "do" statements "end") - (non-block-expr "fn" match-statement "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 "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) - ("(" statements ")") - ("{" statements "}") - ("[" statements "]") - ("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"))))) - -(defvar elixir-smie-indent-basic 2) + (let ((pos (point))) + (forward-comment (- (point))) + (cond + ((and (> pos (line-end-position)) + (elixir-smie--implicit-semi-p)) + ";") + ((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))) @@ -256,35 +131,25 @@ Return non-nil if any line breaks were skipped." (defun elixir-smie-rules (kind token) (pcase (cons kind token) - (`(:after . "STRING") - (if (smie-rule-prev-p "do:") - (smie-rule-parent 0) - nil)) - (`(:elem . basic) - (if (smie-rule-hanging-p) - 0 - elixir-smie-indent-basic)) (`(:after . "OP") - (unless (smie-rule-sibling-p) - elixir-smie-indent-basic)) + (cond + ((smie-rule-sibling-p) nil) + ((smie-rule-hanging-p) (smie-rule-parent elixir-smie-indent-basic)) + (t elixir-smie-indent-basic))) (`(:before . "def") elixir-smie-indent-basic) ;; If the parent token of `->' is `fn', then we want to align to the ;; parent, and offset by `elixir-smie-indent-basic'. Otherwise, indent ;; normally. This helps us work with/indent anonymous function blocks ;; correctly. - (`(:after . "->") - (when (smie-rule-hanging-p) - (if (smie-rule-parent-p "fn") - (smie-rule-parent elixir-smie-indent-basic) - elixir-smie-indent-basic))) - (`(:after . "do") - elixir-smie-indent-basic) - (`(:list-intro . ,(or `"do" `";")) t) + (`(: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 . ";") (if (smie-rule-parent-p "if") (smie-rule-parent 0))))) - (define-minor-mode elixir-smie-mode "SMIE-based indentation and syntax for Elixir" nil nil nil nil diff --git a/test/elixir-mode-indentation-tests.el b/test/elixir-mode-indentation-tests.el index 6d05f1cd..b638c738 100644 --- a/test/elixir-mode-indentation-tests.el +++ b/test/elixir-mode-indentation-tests.el @@ -176,26 +176,36 @@ end (elixir-def-indentation-test indents-records-correctly () " -defrecord Money, [:currency_unit, :amount] do -foo +defmodule MyModule do +require Record +Record.defrecord :money, [:currency_unit, :amount] + +Record.defrecord :animal, [:species, :name] end " " -defrecord Money, [:currency_unit, :amount] do - foo +defmodule MyModule do + require Record + Record.defrecord :money, [:currency_unit, :amount] + + Record.defrecord :animal, [:species, :name] end ") (elixir-def-indentation-test indents-continuation-lines () " -has_something(x) && -has_something(y) || -has_something(z) -" - " +def foo do has_something(x) && has_something(y) || has_something(z) +end +" + " +def foo do + has_something(x) && + has_something(y) || + has_something(z) +end ") (elixir-def-indentation-test indents-continuation-lines-with-comments/1 diff --git a/test/elixir-mode-tokenizer-hl-tests.el b/test/elixir-mode-tokenizer-hl-tests.el deleted file mode 100644 index bf77ed9d..00000000 --- a/test/elixir-mode-tokenizer-hl-tests.el +++ /dev/null @@ -1,34 +0,0 @@ -;;; High-level Tokenizer tests: - -(elixir-deftest skip-forwards-over-comments () - (elixir-ert-with-test-buffer () - "foo # comment\nbar" - (goto-char (point-min)) - (should (equal "foo" (elixir-smie-next-token t))) - (should (equal ";" (elixir-smie-next-token t))) - (should (equal "bar" (elixir-smie-next-token t))) - (should (equal (point-max) (point))))) - -(elixir-deftest skip-backwards-over-comments () - (elixir-ert-with-test-buffer () - "foo # comment\nbar" - (goto-char (point-max)) - (should (equal "bar" (elixir-smie-next-token nil))) - (should (equal ";" (elixir-smie-next-token nil))) - (should (equal "foo" (elixir-smie-next-token nil))) - (should (equal (point-min) (point))))) - -(elixir-deftest treat-comments-the-same-as-whitespace-backwards () - (let ((skipped-to-posn)) - (elixir-ert-with-test-buffer (:name "With comment") - "function do # test\n bar\nend" - (goto-char (point-min)) - (forward-line 1) - (should (equal "do" (elixir-smie-next-token nil))) - (setq skipped-to-posn (point))) - (elixir-ert-with-test-buffer (:name "Without comment") - "function do \n bar\nend" - (goto-char (point-min)) - (forward-line 1) - (should (equal "do" (elixir-smie-next-token nil))) - (should (equal skipped-to-posn (point))))))