diff options
| -rw-r--r-- | rust-mode-tests.el | 220 | ||||
| -rw-r--r-- | rust-mode.el | 82 |
2 files changed, 302 insertions, 0 deletions
diff --git a/rust-mode-tests.el b/rust-mode-tests.el index 614340c..759f919 100644 --- a/rust-mode-tests.el +++ b/rust-mode-tests.el @@ -2024,6 +2024,226 @@ fn main() { '("r#\"\"\"#" font-lock-string-face "'q'" font-lock-string-face))) +(ert-deftest rust-macro-font-lock () + (rust-test-font-lock + "foo!\(\);" + '("foo!" font-lock-preprocessor-face)) + (rust-test-font-lock + "foo!{};" + '("foo!" font-lock-preprocessor-face)) + (rust-test-font-lock + "foo![];" + '("foo!" font-lock-preprocessor-face))) + +(ert-deftest rust-string-interpolation-matcher-works () + (dolist (test '(("print!\(\"\"\)" 9 11 nil) + ("print!\(\"abcd\"\)" 9 15 nil) + ("print!\(\"abcd {{}}\"\);" 9 19 nil) + ("print!\(\"abcd {{\"\);" 9 18 nil) + ("print!\(\"abcd {}\"\);" 9 18 ((14 16))) + ("print!\(\"abcd {{{}\"\);" 9 20 ((16 18))) + ("print!\(\"abcd {}{{\"\);" 9 20 ((14 16))) + ("print!\(\"abcd {} {{\"\);" 9 21 ((14 16))) + ("print!\(\"abcd {}}}\"\);" 9 20 ((14 16))) + ("print!\(\"abcd {{{}}}\"\);" 9 20 ((16 18))) + ("print!\(\"abcd {0}\"\);" 9 18 ((14 17))) + ("print!\(\"abcd {0} efgh\"\);" 9 23 ((14 17))) + ("print!\(\"{1} abcd {0} efgh\"\);" 9 27 ((9 12) (18 21))) + ("print!\(\"{{{1} abcd }} {0}}} {{efgh}}\"\);" 9 33 ((11 14) (23 26))))) + (destructuring-bind (text cursor limit matches) test + (with-temp-buffer + ;; make sure we have a clean slate + (save-match-data + (set-match-data nil) + (insert text) + (goto-char cursor) + (if (null matches) + (should (equal (progn + (rust-string-interpolation-matcher limit) + (match-data)) + nil)) + (dolist (pair matches) + (rust-string-interpolation-matcher limit) + (should (equal (match-beginning 0) (car pair))) + (should (equal (match-end 0) (cadr pair)))))))))) + +(ert-deftest rust-formatting-macro-font-lock () + ;; test that the block delimiters aren't highlighted and the comment + ;; is ignored + (rust-test-font-lock + "print!(\"\"); { /* print!(\"\"); */ }" + '("print!" rust-builtin-formatting-macro-face + "\"\"" font-lock-string-face + "/* " font-lock-comment-delimiter-face + "print!(\"\"); */" font-lock-comment-face)) + ;; other delimiters + (rust-test-font-lock + "print!{\"\"}; { /* no-op */ }" + '("print!" rust-builtin-formatting-macro-face + "\"\"" font-lock-string-face + "/* " font-lock-comment-delimiter-face + "no-op */" font-lock-comment-face)) + ;; other delimiters + (rust-test-font-lock + "print![\"\"]; { /* no-op */ }" + '("print!" rust-builtin-formatting-macro-face + "\"\"" font-lock-string-face + "/* " font-lock-comment-delimiter-face + "no-op */" font-lock-comment-face)) + ;; no interpolation + (rust-test-font-lock + "print!(\"abcd\"); { /* no-op */ }" + '("print!" rust-builtin-formatting-macro-face + "\"abcd\"" font-lock-string-face + "/* " font-lock-comment-delimiter-face + "no-op */" font-lock-comment-face)) + ;; only interpolation + (rust-test-font-lock + "print!(\"{}\"); { /* no-op */ }" + '("print!" rust-builtin-formatting-macro-face + "\"" font-lock-string-face + "{}" rust-string-interpolation-face + "\"" font-lock-string-face + "/* " font-lock-comment-delimiter-face + "no-op */" font-lock-comment-face)) + ;; text + interpolation + (rust-test-font-lock + "print!(\"abcd {}\", foo); { /* no-op */ }" + '("print!" rust-builtin-formatting-macro-face + "\"abcd " font-lock-string-face + "{}" rust-string-interpolation-face + "\"" font-lock-string-face + "/* " font-lock-comment-delimiter-face + "no-op */" font-lock-comment-face)) + ;; text + interpolation with specification + (rust-test-font-lock + "print!(\"abcd {0}\", foo); { /* no-op */ }" + '("print!" rust-builtin-formatting-macro-face + "\"abcd " font-lock-string-face + "{0}" rust-string-interpolation-face + "\"" font-lock-string-face + "/* " font-lock-comment-delimiter-face + "no-op */" font-lock-comment-face)) + ;; text + interpolation with specification and escape + (rust-test-font-lock + "print!(\"abcd {0}}}\", foo); { /* no-op */ }" + '("print!" rust-builtin-formatting-macro-face + "\"abcd " font-lock-string-face + "{0}" rust-string-interpolation-face + "}}\"" font-lock-string-face + "/* " font-lock-comment-delimiter-face + "no-op */" font-lock-comment-face)) + ;; multiple pairs + (rust-test-font-lock + "print!(\"abcd {0} efgh {1}\", foo, bar); { /* no-op */ }" + '("print!" rust-builtin-formatting-macro-face + "\"abcd " font-lock-string-face + "{0}" rust-string-interpolation-face + " efgh " font-lock-string-face + "{1}" rust-string-interpolation-face + "\"" font-lock-string-face + "/* " font-lock-comment-delimiter-face + "no-op */" font-lock-comment-face)) + ;; println + (rust-test-font-lock + "println!(\"abcd {0} efgh {1}\", foo, bar); { /* no-op */ }" + '("println!" rust-builtin-formatting-macro-face + "\"abcd " font-lock-string-face + "{0}" rust-string-interpolation-face + " efgh " font-lock-string-face + "{1}" rust-string-interpolation-face + "\"" font-lock-string-face + "/* " font-lock-comment-delimiter-face + "no-op */" font-lock-comment-face)) + ;; eprint + (rust-test-font-lock + "eprint!(\"abcd {0} efgh {1}\", foo, bar); { /* no-op */ }" + '("eprint!" rust-builtin-formatting-macro-face + "\"abcd " font-lock-string-face + "{0}" rust-string-interpolation-face + " efgh " font-lock-string-face + "{1}" rust-string-interpolation-face + "\"" font-lock-string-face + "/* " font-lock-comment-delimiter-face + "no-op */" font-lock-comment-face)) + ;; eprintln + (rust-test-font-lock + "eprintln!(\"abcd {0} efgh {1}\", foo, bar); { /* no-op */ }" + '("eprintln!" rust-builtin-formatting-macro-face + "\"abcd " font-lock-string-face + "{0}" rust-string-interpolation-face + " efgh " font-lock-string-face + "{1}" rust-string-interpolation-face + "\"" font-lock-string-face + "/* " font-lock-comment-delimiter-face + "no-op */" font-lock-comment-face)) + ;; format + (rust-test-font-lock + "format!(\"abcd {0} efgh {1}\", foo, bar); { /* no-op */ }" + '("format!" rust-builtin-formatting-macro-face + "\"abcd " font-lock-string-face + "{0}" rust-string-interpolation-face + " efgh " font-lock-string-face + "{1}" rust-string-interpolation-face + "\"" font-lock-string-face + "/* " font-lock-comment-delimiter-face + "no-op */" font-lock-comment-face)) + ;; print + raw string + (rust-test-font-lock + "format!(r\"abcd {0} efgh {1}\", foo, bar); { /* no-op */ }" + '("format!" rust-builtin-formatting-macro-face + "r\"abcd " font-lock-string-face + "{0}" rust-string-interpolation-face + " efgh " font-lock-string-face + "{1}" rust-string-interpolation-face + "\"" font-lock-string-face + "/* " font-lock-comment-delimiter-face + "no-op */" font-lock-comment-face)) + ;; print + raw string with hash + (rust-test-font-lock + "format!(r#\"abcd {0} efgh {1}\"#, foo, bar); { /* no-op */ }" + '("format!" rust-builtin-formatting-macro-face + "r#\"abcd " font-lock-string-face + "{0}" rust-string-interpolation-face + " efgh " font-lock-string-face + "{1}" rust-string-interpolation-face + "\"#" font-lock-string-face + "/* " font-lock-comment-delimiter-face + "no-op */" font-lock-comment-face)) + ;; print + raw string with two hashes + (rust-test-font-lock + "format!(r##\"abcd {0} efgh {1}\"##, foo, bar); { /* no-op */ }" + '("format!" rust-builtin-formatting-macro-face + "r##\"abcd " font-lock-string-face + "{0}" rust-string-interpolation-face + " efgh " font-lock-string-face + "{1}" rust-string-interpolation-face + "\"##" font-lock-string-face + "/* " font-lock-comment-delimiter-face + "no-op */" font-lock-comment-face))) + +(ert-deftest rust-write-macro-font-lock () + (rust-test-font-lock + "write!(f, \"abcd {0}}} efgh {1}\", foo, bar); { /* no-op */ }" + '("write!" rust-builtin-formatting-macro-face + "\"abcd " font-lock-string-face + "{0}" rust-string-interpolation-face + "}} efgh " font-lock-string-face + "{1}" rust-string-interpolation-face + "\"" font-lock-string-face + "/* " font-lock-comment-delimiter-face + "no-op */" font-lock-comment-face)) + (rust-test-font-lock + "writeln!(f, \"abcd {0}}} efgh {1}\", foo, bar); { /* no-op */ }" + '("writeln!" rust-builtin-formatting-macro-face + "\"abcd " font-lock-string-face + "{0}" rust-string-interpolation-face + "}} efgh " font-lock-string-face + "{1}" rust-string-interpolation-face + "\"" font-lock-string-face + "/* " font-lock-comment-delimiter-face + "no-op */" font-lock-comment-face))) + (ert-deftest rust-test-basic-paren-matching () (rust-test-matching-parens " diff --git a/rust-mode.el b/rust-mode.el index eac246f..8ae2454 100644 --- a/rust-mode.el +++ b/rust-mode.el @@ -170,7 +170,18 @@ function or trait. When nil, where will be aligned with fn or trait." "Face for the question mark operator." :group 'rust-mode) +(defface rust-builtin-formatting-macro-face + '((t :inherit font-lock-builtin-face)) + "Face for builtin formatting macros (print! &c.)." + :group 'rust-mode) + +(defface rust-string-interpolation-face + '((t :slant italic :inherit font-lock-string-face)) + "Face for interpolating braces in builtin formatting macro strings." + :group 'rust-mode) + (defun rust-paren-level () (nth 0 (syntax-ppss))) +(defun rust-in-str () (nth 3 (syntax-ppss))) (defun rust-in-str-or-cmnt () (nth 8 (syntax-ppss))) (defun rust-rewind-past-str-cmnt () (goto-char (nth 8 (syntax-ppss)))) @@ -573,6 +584,54 @@ the desired identifiers), but does not match type annotations \"foo::<\"." ((not (looking-at (rx (0+ space) "<"))) (throw 'rust-path-font-lock-matcher match)))))))) +(defun rust-next-string-interpolation (limit) + "Search forward from point for next Rust interpolation marker +before LIMIT. +Set point to the end of the occurrence found, and return match beginning +and end." + (catch 'match + (save-match-data + (save-excursion + (while (search-forward "{" limit t) + (if (eql (char-after (point)) ?{) + (forward-char) + (let ((start (match-beginning 0))) + ;; According to fmt_macros::Parser::next, an opening brace + ;; must be followed by an optional argument and/or format + ;; specifier, then a closing brace. A single closing brace + ;; without a corresponding unescaped opening brace is an + ;; error. We don't need to do anything special with + ;; arguments, specifiers, or errors, so we only search for + ;; the single closing brace. + (when (search-forward "}" limit t) + (throw 'match (list start (point))))))))))) + +(defun rust-string-interpolation-matcher (limit) + "Match next Rust interpolation marker before LIMIT and set +match data if found. Returns nil if not within a Rust string." + (when (rust-in-str) + (let ((match (rust-next-string-interpolation limit))) + (when match + (set-match-data match) + (goto-char (cadr match)) + match)))) + +(defvar rust-builtin-formatting-macros + '("eprint" + "eprintln" + "format" + "print" + "println") + "List of builtin Rust macros for string formatting used by `rust-mode-font-lock-keywords'. (`write!' is handled separately.)") + +(defvar rust-formatting-macro-opening-re + "[[:space:]]*[({[][[:space:]]*" + "Regular expression to match the opening delimiter of a Rust formatting macro.") + +(defvar rust-start-of-string-re + "\\(?:r#*\\)?\"" + "Regular expression to match the start of a Rust raw string.") + (defvar rust-mode-font-lock-keywords (append `( @@ -590,6 +649,22 @@ the desired identifiers), but does not match type annotations \"foo::<\"." (,(rust-re-grab (concat "#\\!?\\[" rust-re-ident "[^]]*\\]")) 1 font-lock-preprocessor-face keep) + ;; Builtin formatting macros + (,(concat (rust-re-grab (concat (regexp-opt rust-builtin-formatting-macros) "!")) (concat rust-formatting-macro-opening-re rust-start-of-string-re)) + (1 'rust-builtin-formatting-macro-face) + (rust-string-interpolation-matcher + (rust-end-of-string) + nil + (0 'rust-string-interpolation-face t nil))) + + ;; write! macro + (,(concat (rust-re-grab "write\\(ln\\)?!") (concat rust-formatting-macro-opening-re "[[:space:]]*[^\"]+,[[:space:]]*" rust-start-of-string-re)) + (1 'rust-builtin-formatting-macro-face) + (rust-string-interpolation-matcher + (rust-end-of-string) + nil + (0 'rust-string-interpolation-face t nil))) + ;; Syntax extension invocations like `foo!`, highlight including the ! (,(concat (rust-re-grab (concat rust-re-ident "!")) "[({[:space:][]") 1 font-lock-preprocessor-face) @@ -1215,6 +1290,13 @@ This is written mainly to be used as `end-of-defun-function' for Rust." ;; There is no opening brace, so consider the whole buffer to be one "defun" (goto-char (point-max)))) +(defun rust-end-of-string () + "Skip to the end of the current string." + (save-excursion + (skip-syntax-forward "^\"|") + (skip-syntax-forward "\"|") + (point))) + ;; Formatting using rustfmt (defun rust--format-call (buf) "Format BUF using rustfmt." |
