Skip to content

Commit 89320ad

Browse files
committed
Highlight interpolation in arguments to print! &c.
1 parent 09efc45 commit 89320ad

File tree

2 files changed

+302
-0
lines changed

2 files changed

+302
-0
lines changed

rust-mode-tests.el

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2024,6 +2024,226 @@ fn main() {
20242024
'("r#\"\"\"#" font-lock-string-face
20252025
"'q'" font-lock-string-face)))
20262026

2027+
(ert-deftest rust-macro-font-lock ()
2028+
(rust-test-font-lock
2029+
"foo!\(\);"
2030+
'("foo!" font-lock-preprocessor-face))
2031+
(rust-test-font-lock
2032+
"foo!{};"
2033+
'("foo!" font-lock-preprocessor-face))
2034+
(rust-test-font-lock
2035+
"foo![];"
2036+
'("foo!" font-lock-preprocessor-face)))
2037+
2038+
(ert-deftest rust-string-interpolation-matcher-works ()
2039+
(dolist (test '(("print!\(\"\"\)" 9 11 nil)
2040+
("print!\(\"abcd\"\)" 9 15 nil)
2041+
("print!\(\"abcd {{}}\"\);" 9 19 nil)
2042+
("print!\(\"abcd {{\"\);" 9 18 nil)
2043+
("print!\(\"abcd {}\"\);" 9 18 ((14 16)))
2044+
("print!\(\"abcd {{{}\"\);" 9 20 ((16 18)))
2045+
("print!\(\"abcd {}{{\"\);" 9 20 ((14 16)))
2046+
("print!\(\"abcd {} {{\"\);" 9 21 ((14 16)))
2047+
("print!\(\"abcd {}}}\"\);" 9 20 ((14 16)))
2048+
("print!\(\"abcd {{{}}}\"\);" 9 20 ((16 18)))
2049+
("print!\(\"abcd {0}\"\);" 9 18 ((14 17)))
2050+
("print!\(\"abcd {0} efgh\"\);" 9 23 ((14 17)))
2051+
("print!\(\"{1} abcd {0} efgh\"\);" 9 27 ((9 12) (18 21)))
2052+
("print!\(\"{{{1} abcd }} {0}}} {{efgh}}\"\);" 9 33 ((11 14) (23 26)))))
2053+
(destructuring-bind (text cursor limit matches) test
2054+
(with-temp-buffer
2055+
;; make sure we have a clean slate
2056+
(save-match-data
2057+
(set-match-data nil)
2058+
(insert text)
2059+
(goto-char cursor)
2060+
(if (null matches)
2061+
(should (equal (progn
2062+
(rust-string-interpolation-matcher limit)
2063+
(match-data))
2064+
nil))
2065+
(dolist (pair matches)
2066+
(rust-string-interpolation-matcher limit)
2067+
(should (equal (match-beginning 0) (car pair)))
2068+
(should (equal (match-end 0) (cadr pair))))))))))
2069+
2070+
(ert-deftest rust-formatting-macro-font-lock ()
2071+
;; test that the block delimiters aren't highlighted and the comment
2072+
;; is ignored
2073+
(rust-test-font-lock
2074+
"print!(\"\"); { /* print!(\"\"); */ }"
2075+
'("print!" rust-builtin-formatting-macro-face
2076+
"\"\"" font-lock-string-face
2077+
"/* " font-lock-comment-delimiter-face
2078+
"print!(\"\"); */" font-lock-comment-face))
2079+
;; other delimiters
2080+
(rust-test-font-lock
2081+
"print!{\"\"}; { /* no-op */ }"
2082+
'("print!" rust-builtin-formatting-macro-face
2083+
"\"\"" font-lock-string-face
2084+
"/* " font-lock-comment-delimiter-face
2085+
"no-op */" font-lock-comment-face))
2086+
;; other delimiters
2087+
(rust-test-font-lock
2088+
"print![\"\"]; { /* no-op */ }"
2089+
'("print!" rust-builtin-formatting-macro-face
2090+
"\"\"" font-lock-string-face
2091+
"/* " font-lock-comment-delimiter-face
2092+
"no-op */" font-lock-comment-face))
2093+
;; no interpolation
2094+
(rust-test-font-lock
2095+
"print!(\"abcd\"); { /* no-op */ }"
2096+
'("print!" rust-builtin-formatting-macro-face
2097+
"\"abcd\"" font-lock-string-face
2098+
"/* " font-lock-comment-delimiter-face
2099+
"no-op */" font-lock-comment-face))
2100+
;; only interpolation
2101+
(rust-test-font-lock
2102+
"print!(\"{}\"); { /* no-op */ }"
2103+
'("print!" rust-builtin-formatting-macro-face
2104+
"\"" font-lock-string-face
2105+
"{}" rust-string-interpolation-face
2106+
"\"" font-lock-string-face
2107+
"/* " font-lock-comment-delimiter-face
2108+
"no-op */" font-lock-comment-face))
2109+
;; text + interpolation
2110+
(rust-test-font-lock
2111+
"print!(\"abcd {}\", foo); { /* no-op */ }"
2112+
'("print!" rust-builtin-formatting-macro-face
2113+
"\"abcd " font-lock-string-face
2114+
"{}" rust-string-interpolation-face
2115+
"\"" font-lock-string-face
2116+
"/* " font-lock-comment-delimiter-face
2117+
"no-op */" font-lock-comment-face))
2118+
;; text + interpolation with specification
2119+
(rust-test-font-lock
2120+
"print!(\"abcd {0}\", foo); { /* no-op */ }"
2121+
'("print!" rust-builtin-formatting-macro-face
2122+
"\"abcd " font-lock-string-face
2123+
"{0}" rust-string-interpolation-face
2124+
"\"" font-lock-string-face
2125+
"/* " font-lock-comment-delimiter-face
2126+
"no-op */" font-lock-comment-face))
2127+
;; text + interpolation with specification and escape
2128+
(rust-test-font-lock
2129+
"print!(\"abcd {0}}}\", foo); { /* no-op */ }"
2130+
'("print!" rust-builtin-formatting-macro-face
2131+
"\"abcd " font-lock-string-face
2132+
"{0}" rust-string-interpolation-face
2133+
"}}\"" font-lock-string-face
2134+
"/* " font-lock-comment-delimiter-face
2135+
"no-op */" font-lock-comment-face))
2136+
;; multiple pairs
2137+
(rust-test-font-lock
2138+
"print!(\"abcd {0} efgh {1}\", foo, bar); { /* no-op */ }"
2139+
'("print!" rust-builtin-formatting-macro-face
2140+
"\"abcd " font-lock-string-face
2141+
"{0}" rust-string-interpolation-face
2142+
" efgh " font-lock-string-face
2143+
"{1}" rust-string-interpolation-face
2144+
"\"" font-lock-string-face
2145+
"/* " font-lock-comment-delimiter-face
2146+
"no-op */" font-lock-comment-face))
2147+
;; println
2148+
(rust-test-font-lock
2149+
"println!(\"abcd {0} efgh {1}\", foo, bar); { /* no-op */ }"
2150+
'("println!" rust-builtin-formatting-macro-face
2151+
"\"abcd " font-lock-string-face
2152+
"{0}" rust-string-interpolation-face
2153+
" efgh " font-lock-string-face
2154+
"{1}" rust-string-interpolation-face
2155+
"\"" font-lock-string-face
2156+
"/* " font-lock-comment-delimiter-face
2157+
"no-op */" font-lock-comment-face))
2158+
;; eprint
2159+
(rust-test-font-lock
2160+
"eprint!(\"abcd {0} efgh {1}\", foo, bar); { /* no-op */ }"
2161+
'("eprint!" rust-builtin-formatting-macro-face
2162+
"\"abcd " font-lock-string-face
2163+
"{0}" rust-string-interpolation-face
2164+
" efgh " font-lock-string-face
2165+
"{1}" rust-string-interpolation-face
2166+
"\"" font-lock-string-face
2167+
"/* " font-lock-comment-delimiter-face
2168+
"no-op */" font-lock-comment-face))
2169+
;; eprintln
2170+
(rust-test-font-lock
2171+
"eprintln!(\"abcd {0} efgh {1}\", foo, bar); { /* no-op */ }"
2172+
'("eprintln!" rust-builtin-formatting-macro-face
2173+
"\"abcd " font-lock-string-face
2174+
"{0}" rust-string-interpolation-face
2175+
" efgh " font-lock-string-face
2176+
"{1}" rust-string-interpolation-face
2177+
"\"" font-lock-string-face
2178+
"/* " font-lock-comment-delimiter-face
2179+
"no-op */" font-lock-comment-face))
2180+
;; format
2181+
(rust-test-font-lock
2182+
"format!(\"abcd {0} efgh {1}\", foo, bar); { /* no-op */ }"
2183+
'("format!" rust-builtin-formatting-macro-face
2184+
"\"abcd " font-lock-string-face
2185+
"{0}" rust-string-interpolation-face
2186+
" efgh " font-lock-string-face
2187+
"{1}" rust-string-interpolation-face
2188+
"\"" font-lock-string-face
2189+
"/* " font-lock-comment-delimiter-face
2190+
"no-op */" font-lock-comment-face))
2191+
;; print + raw string
2192+
(rust-test-font-lock
2193+
"format!(r\"abcd {0} efgh {1}\", foo, bar); { /* no-op */ }"
2194+
'("format!" rust-builtin-formatting-macro-face
2195+
"r\"abcd " font-lock-string-face
2196+
"{0}" rust-string-interpolation-face
2197+
" efgh " font-lock-string-face
2198+
"{1}" rust-string-interpolation-face
2199+
"\"" font-lock-string-face
2200+
"/* " font-lock-comment-delimiter-face
2201+
"no-op */" font-lock-comment-face))
2202+
;; print + raw string with hash
2203+
(rust-test-font-lock
2204+
"format!(r#\"abcd {0} efgh {1}\"#, foo, bar); { /* no-op */ }"
2205+
'("format!" rust-builtin-formatting-macro-face
2206+
"r#\"abcd " font-lock-string-face
2207+
"{0}" rust-string-interpolation-face
2208+
" efgh " font-lock-string-face
2209+
"{1}" rust-string-interpolation-face
2210+
"\"#" font-lock-string-face
2211+
"/* " font-lock-comment-delimiter-face
2212+
"no-op */" font-lock-comment-face))
2213+
;; print + raw string with two hashes
2214+
(rust-test-font-lock
2215+
"format!(r##\"abcd {0} efgh {1}\"##, foo, bar); { /* no-op */ }"
2216+
'("format!" rust-builtin-formatting-macro-face
2217+
"r##\"abcd " font-lock-string-face
2218+
"{0}" rust-string-interpolation-face
2219+
" efgh " font-lock-string-face
2220+
"{1}" rust-string-interpolation-face
2221+
"\"##" font-lock-string-face
2222+
"/* " font-lock-comment-delimiter-face
2223+
"no-op */" font-lock-comment-face)))
2224+
2225+
(ert-deftest rust-write-macro-font-lock ()
2226+
(rust-test-font-lock
2227+
"write!(f, \"abcd {0}}} efgh {1}\", foo, bar); { /* no-op */ }"
2228+
'("write!" rust-builtin-formatting-macro-face
2229+
"\"abcd " font-lock-string-face
2230+
"{0}" rust-string-interpolation-face
2231+
"}} efgh " font-lock-string-face
2232+
"{1}" rust-string-interpolation-face
2233+
"\"" font-lock-string-face
2234+
"/* " font-lock-comment-delimiter-face
2235+
"no-op */" font-lock-comment-face))
2236+
(rust-test-font-lock
2237+
"writeln!(f, \"abcd {0}}} efgh {1}\", foo, bar); { /* no-op */ }"
2238+
'("writeln!" rust-builtin-formatting-macro-face
2239+
"\"abcd " font-lock-string-face
2240+
"{0}" rust-string-interpolation-face
2241+
"}} efgh " font-lock-string-face
2242+
"{1}" rust-string-interpolation-face
2243+
"\"" font-lock-string-face
2244+
"/* " font-lock-comment-delimiter-face
2245+
"no-op */" font-lock-comment-face)))
2246+
20272247
(ert-deftest rust-test-basic-paren-matching ()
20282248
(rust-test-matching-parens
20292249
"

rust-mode.el

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,18 @@ function or trait. When nil, where will be aligned with fn or trait."
170170
"Face for the question mark operator."
171171
:group 'rust-mode)
172172

173+
(defface rust-builtin-formatting-macro-face
174+
'((t :inherit font-lock-builtin-face))
175+
"Face for builtin formatting macros (print! &c.)."
176+
:group 'rust-mode)
177+
178+
(defface rust-string-interpolation-face
179+
'((t :slant italic :inherit font-lock-string-face))
180+
"Face for interpolating braces in builtin formatting macro strings."
181+
:group 'rust-mode)
182+
173183
(defun rust-paren-level () (nth 0 (syntax-ppss)))
184+
(defun rust-in-str () (nth 3 (syntax-ppss)))
174185
(defun rust-in-str-or-cmnt () (nth 8 (syntax-ppss)))
175186
(defun rust-rewind-past-str-cmnt () (goto-char (nth 8 (syntax-ppss))))
176187

@@ -573,6 +584,54 @@ the desired identifiers), but does not match type annotations \"foo::<\"."
573584
((not (looking-at (rx (0+ space) "<")))
574585
(throw 'rust-path-font-lock-matcher match))))))))
575586

587+
(defun rust-next-string-interpolation (limit)
588+
"Search forward from point for next Rust interpolation marker
589+
before LIMIT.
590+
Set point to the end of the occurrence found, and return match beginning
591+
and end."
592+
(catch 'match
593+
(save-match-data
594+
(save-excursion
595+
(while (search-forward "{" limit t)
596+
(if (eql (char-after (point)) ?{)
597+
(forward-char)
598+
(let ((start (match-beginning 0)))
599+
;; According to fmt_macros::Parser::next, an opening brace
600+
;; must be followed by an optional argument and/or format
601+
;; specifier, then a closing brace. A single closing brace
602+
;; without a corresponding unescaped opening brace is an
603+
;; error. We don't need to do anything special with
604+
;; arguments, specifiers, or errors, so we only search for
605+
;; the single closing brace.
606+
(when (search-forward "}" limit t)
607+
(throw 'match (list start (point)))))))))))
608+
609+
(defun rust-string-interpolation-matcher (limit)
610+
"Match next Rust interpolation marker before LIMIT and set
611+
match data if found. Returns nil if not within a Rust string."
612+
(when (rust-in-str)
613+
(let ((match (rust-next-string-interpolation limit)))
614+
(when match
615+
(set-match-data match)
616+
(goto-char (cadr match))
617+
match))))
618+
619+
(defvar rust-builtin-formatting-macros
620+
'("eprint"
621+
"eprintln"
622+
"format"
623+
"print"
624+
"println")
625+
"List of builtin Rust macros for string formatting used by `rust-mode-font-lock-keywords'. (`write!' is handled separately.)")
626+
627+
(defvar rust-formatting-macro-opening-re
628+
"[[:space:]]*[({[][[:space:]]*"
629+
"Regular expression to match the opening delimiter of a Rust formatting macro.")
630+
631+
(defvar rust-start-of-string-re
632+
"\\(?:r#*\\)?\""
633+
"Regular expression to match the start of a Rust raw string.")
634+
576635
(defvar rust-mode-font-lock-keywords
577636
(append
578637
`(
@@ -590,6 +649,22 @@ the desired identifiers), but does not match type annotations \"foo::<\"."
590649
(,(rust-re-grab (concat "#\\!?\\[" rust-re-ident "[^]]*\\]"))
591650
1 font-lock-preprocessor-face keep)
592651

652+
;; Builtin formatting macros
653+
(,(concat (rust-re-grab (concat (regexp-opt rust-builtin-formatting-macros) "!")) (concat rust-formatting-macro-opening-re rust-start-of-string-re))
654+
(1 'rust-builtin-formatting-macro-face)
655+
(rust-string-interpolation-matcher
656+
(rust-end-of-string)
657+
nil
658+
(0 'rust-string-interpolation-face t nil)))
659+
660+
;; write! macro
661+
(,(concat (rust-re-grab "write\\(ln\\)?!") (concat rust-formatting-macro-opening-re "[[:space:]]*[^\"]+,[[:space:]]*" rust-start-of-string-re))
662+
(1 'rust-builtin-formatting-macro-face)
663+
(rust-string-interpolation-matcher
664+
(rust-end-of-string)
665+
nil
666+
(0 'rust-string-interpolation-face t nil)))
667+
593668
;; Syntax extension invocations like `foo!`, highlight including the !
594669
(,(concat (rust-re-grab (concat rust-re-ident "!")) "[({[:space:][]")
595670
1 font-lock-preprocessor-face)
@@ -1215,6 +1290,13 @@ This is written mainly to be used as `end-of-defun-function' for Rust."
12151290
;; There is no opening brace, so consider the whole buffer to be one "defun"
12161291
(goto-char (point-max))))
12171292

1293+
(defun rust-end-of-string ()
1294+
"Skip to the end of the current string."
1295+
(save-excursion
1296+
(skip-syntax-forward "^\"|")
1297+
(skip-syntax-forward "\"|")
1298+
(point)))
1299+
12181300
;; Formatting using rustfmt
12191301
(defun rust--format-call (buf)
12201302
"Format BUF using rustfmt."

0 commit comments

Comments
 (0)