From 53c558cf7241cbeca93f18e04a81a25ef8675313 Mon Sep 17 00:00:00 2001 From: Micah Chalmer Date: Mon, 8 Jun 2015 02:04:05 -0400 Subject: Factor out rust-conditional-re-search-forward Factor out the method of looking for a match for a regexp, but filtering out some of the matches with a filtering function. This will be used again for angle bracket filtering. This also fixes an issue with raw string handling. --- rust-mode-tests.el | 23 +++++++++++++++++++++++ rust-mode.el | 53 ++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/rust-mode-tests.el b/rust-mode-tests.el index 58fcaaf..c7446d6 100644 --- a/rust-mode-tests.el +++ b/rust-mode-tests.el @@ -362,6 +362,29 @@ use foo::bar::baz; fn foo() { } ")) +(ert-deftest font-lock-multi-raw-strings-in-a-row () + (rust-test-font-lock + " +r\"foo\\\", \"bar\", r\"bar\"; +r\"foo\\.\", \"bar\", r\"bar\"; +r\"foo\\..\", \"bar\", r\"foo\\..\\bar\"; +r\"\\\", \"foo\", r\"\\foo\"; +not_a_string(); + +" + + (apply 'append (mapcar (lambda (s) (list s 'font-lock-string-face)) + '("r\"foo\\\"" "\"bar\"" "r\"bar\"" + "r\"foo\\.\"" "\"bar\"" "r\"bar\"" + "r\"foo\\..\"" "\"bar\"" "r\"foo\\..\\bar\"" + "r\"\\\"" "\"foo\"" "r\"\\foo\""))) + )) + +(ert-deftest font-lock-raw-string-after-normal-string-ending-in-r () + (rust-test-font-lock + "\"bar\" r\"foo\"" + '("\"bar\"" font-lock-string-face "r\"foo\"" font-lock-string-face))) + (ert-deftest indent-params-no-align () (test-indent " diff --git a/rust-mode.el b/rust-mode.el index 7fcc09e..ae42052 100644 --- a/rust-mode.el +++ b/rust-mode.el @@ -411,6 +411,32 @@ (/= font-lock-end orig-end)) )) +(defun rust-conditional-re-search-forward (regexp bound condition) + ;; Search forward for regexp (with bound). If found, call condition and return the found + ;; match only if it returns true. + (let* (found + found-ret-list + (ret-list (save-excursion + (while (and (not found) (re-search-forward regexp bound t)) + (setq + found-ret-list (list (point) (match-data)) + found (save-match-data (save-excursion (ignore-errors (funcall condition))))) + ;; If the condition filters out a match, need to search + ;; again just after its beginning. This will allow + ;; cases such as: + ;; "bar" r"foo" + ;; where the filtered out search (r" r") should not + ;; prevent finding another one that begins in the middle + ;; of it (r"foo") + (when (not found) + (goto-char (1+ (match-beginning 0)))) + ) + (when found found-ret-list)))) + (when ret-list + (goto-char (nth 0 ret-list)) + (set-match-data (nth 1 ret-list)) + (nth 0 ret-list)))) + (defun rust-look-for-raw-string (bound) ;; Find a raw string, but only if it's not in the middle of another string or ;; a comment @@ -450,23 +476,16 @@ ;; No "#"s - capture the ending quote (using a backref to group 3, ;; so that we can't match a quote if we had "#"s) as group 6 - (group (backref 3)))))) - ;; If it matches, it ends up with the starting character of the string - ;; as group 1, any ending backslashes as group 4, and the ending - ;; character as either group 5 or group 6. - - (ret-list (save-excursion - (let* ((match-end (re-search-forward raw-str-regexp bound t)) - (ret-list (and match-end (list match-end (match-beginning 0) (match-data) (point))))) - (when (and ret-list - (save-excursion - (goto-char (nth 1 ret-list)) - (not (rust-in-str-or-cmnt)))) - ret-list))))) - (when ret-list - (goto-char (nth 3 ret-list)) - (set-match-data (nth 2 ret-list)) - (nth 0 ret-list)))) + (group (backref 3)))) + ;; If it matches, it ends up with the starting character of the string + ;; as group 1, any ending backslashes as group 4, and the ending + ;; character as either group 5 or group 6. + ))) + (rust-conditional-re-search-forward + raw-str-regexp bound + (lambda () (save-excursion + (goto-char (match-beginning 0)) + (not (rust-in-str-or-cmnt))))))) (defvar rust-mode-font-lock-syntactic-keywords (append -- cgit v1.2.3 From 6e72f647da716bf532013484515e5f7ebde60209 Mon Sep 17 00:00:00 2001 From: Micah Chalmer Date: Mon, 8 Jun 2015 01:25:36 -0400 Subject: Fix bugs in raw strings and character literals --- rust-mode-tests.el | 80 +++++++++++++++++++++ rust-mode.el | 202 ++++++++++++++++++++++++++++++----------------------- 2 files changed, 196 insertions(+), 86 deletions(-) diff --git a/rust-mode-tests.el b/rust-mode-tests.el index c7446d6..a846a27 100644 --- a/rust-mode-tests.el +++ b/rust-mode-tests.el @@ -1482,3 +1482,83 @@ la la\"); (test-indent ;; Needs to leave 1 space before "world" "\"hello \\\n world\"")) + +(defun rust-test-matching-parens (content pairs &optional nonparen-positions) + "Assert that in rust-mode, given a buffer with the given `content', + emacs's paren matching will find all of the pairs of positions + as matching braces. The list of nonparen-positions asserts + specific positions that should NOT be considered to be + parens/braces of any kind. + + This does not assert that the `pairs' list is + comprehensive--there can be additional pairs that don't appear + in the list and the test still passes (as long as none of their + positions appear in `nonparen-positions'.)" + (with-temp-buffer + (rust-mode) + (insert content) + (font-lock-fontify-buffer) + (dolist (pair pairs) + (let* ((open-pos (nth 0 pair)) + (close-pos (nth 1 pair))) + (should (equal 4 (syntax-class (syntax-after open-pos)))) + (should (equal 5 (syntax-class (syntax-after close-pos)))) + (should (equal (scan-sexps open-pos 1) (+ 1 close-pos))) + (should (equal (scan-sexps (+ 1 close-pos) -1) open-pos)))) + (dolist (nonpar-pos nonparen-positions) + (let ((nonpar-syntax-class (syntax-class (syntax-after nonpar-pos)))) + (should (not (equal 4 nonpar-syntax-class))) + (should (not (equal 5 nonpar-syntax-class))))))) + +(ert-deftest rust-test-unmatched-single-quote-in-comment-paren-matching () + ;; This was a bug from the char quote handling that affected the paren + ;; matching. An unmatched quote char in a comment caused the problems. + (rust-test-matching-parens + "// If this appeared first in the file... +\"\\ +{\"; + +// And the { was not the on the first column: + { + // This then messed up the paren matching: '\\' +} + +" + '((97 150) ;; The { and } at the bottom + ))) + +(ert-deftest rust-test-two-character-quotes-in-a-row () + (with-temp-buffer + (rust-mode) + (font-lock-fontify-buffer) + (insert "'\\n','a', fn") + (font-lock-after-change-function 1 12 0) + + (should (equal 'font-lock-string-face (get-text-property 3 'face))) + (should (equal nil (get-text-property 5 'face))) + (should (equal 'font-lock-string-face (get-text-property 7 'face))) + (should (equal nil (get-text-property 9 'face))) + (should (equal 'font-lock-keyword-face (get-text-property 12 'face))) + ) + ) + +(ert-deftest single-quote-null-char () + (rust-test-font-lock + "'\\0' 'a' fn" + '("'\\0'" font-lock-string-face + "'a'" font-lock-string-face + "fn" font-lock-keyword-face))) + +(ert-deftest r-in-string-after-single-quoted-double-quote () + (rust-test-font-lock + "'\"';\n\"r\";\n\"oops\";" + '("'\"'" font-lock-string-face + "\"r\"" font-lock-string-face + "\"oops\"" font-lock-string-face + ))) + +(ert-deftest char-literal-after-quote-in-raw-string () + (rust-test-font-lock + "r#\"\"\"#;\n'q'" + '("r#\"\"\"#" font-lock-string-face + "'q'" font-lock-string-face))) diff --git a/rust-mode.el b/rust-mode.el index ae42052..193f672 100644 --- a/rust-mode.el +++ b/rust-mode.el @@ -374,38 +374,53 @@ ("fn" . font-lock-function-name-face) ("static" . font-lock-constant-face))))) -(defun rust-extend-region-raw-string () +(defun rust-font-lock-extend-region () "Extend the region given by `font-lock-beg' and `font-lock-end' - to include the beginning of a string if it includes part of it. - Adjusts to include the r[#] of a raw string as well." - - (let* ((orig-beg font-lock-beg) - (orig-end font-lock-end) - (beg-ppss (syntax-ppss font-lock-beg)) - (beg-in-str (nth 3 beg-ppss)) - (end-ppss (syntax-ppss font-lock-end)) - (end-in-str (nth 3 end-ppss))) - - (when (and beg-in-str (> font-lock-beg (nth 8 beg-ppss))) - (setq font-lock-beg str-beg) - (while (equal ?# (char-before font-lock-beg)) - (setq font-lock-beg (1- font-lock-beg))) - (when (equal ?r (char-before font-lock-beg)) - (setq font-lock-beg (1- font-lock-beg)))) - - (when end-in-str - (save-excursion - (goto-char (nth 8 end-ppss)) - (ignore-errors (forward-sexp)) - (setq font-lock-end (max font-lock-end (point))))) - - ;; If we have the beginning of a raw string in the region, make sure we have the end of - ;; it. - (when (or beg-in-str end-in-str) - (save-excursion - (goto-char font-lock-beg) - (while (and (< (point) font-lock-end) (ignore-errors (rust-look-for-raw-string (buffer-end 1))))) - (setq font-lock-end (max font-lock-end (point))))) + to include the beginning of a string or comment if it includes + part of it. Adjusts to include the r[#] of a raw string as + well." + + (let ((orig-beg font-lock-beg) + (orig-end font-lock-end)) + (cond + ;; If we are not syntactically fontified yet, we cannot correctly cover + ;; anything less than the full buffer. The syntactic fontification + ;; modifies the syntax, so until it's done we can't use the syntax to + ;; determine what to fontify. + ((< (or font-lock-syntactically-fontified 0) font-lock-end) + (setq font-lock-beg 1) + (setq font-lock-end (buffer-end 1))) + + ((let* ((beg-ppss (syntax-ppss font-lock-beg)) + (beg-in-cmnt (and (nth 4 beg-ppss) (nth 8 beg-ppss))) + (beg-in-str (nth 3 beg-ppss)) + (end-ppss (syntax-ppss font-lock-end)) + (end-in-str (nth 3 end-ppss))) + + (when (and beg-in-str (> font-lock-beg (nth 8 beg-ppss))) + (setq font-lock-beg (nth 8 beg-ppss)) + (while (equal ?# (char-before font-lock-beg)) + (setq font-lock-beg (1- font-lock-beg))) + (when (equal ?r (char-before font-lock-beg)) + (setq font-lock-beg (1- font-lock-beg)))) + + (when (and beg-in-cmnt (> font-lock-beg beg-in-cmnt)) + (setq font-lock-beg beg-in-cmnt)) + + (when end-in-str + (save-excursion + (goto-char (nth 8 end-ppss)) + (ignore-errors (forward-sexp)) + (setq font-lock-end (max font-lock-end (point))))) + + ;; If we have the beginning of a raw string in the region, make sure we have the end of + ;; it. + (when (or beg-in-str end-in-str) + (save-excursion + (goto-char font-lock-beg) + (while (and (< (point) font-lock-end) (ignore-errors (rust-look-for-raw-string (buffer-end 1))))) + (setq font-lock-end (max font-lock-end (point))))) + ))) (or (/= font-lock-beg orig-beg) (/= font-lock-end orig-end)) @@ -437,67 +452,82 @@ (set-match-data (nth 1 ret-list)) (nth 0 ret-list)))) -(defun rust-look-for-raw-string (bound) - ;; Find a raw string, but only if it's not in the middle of another string or - ;; a comment +(defun rust-look-for-non-standard-string (bound) + ;; Find a raw string or character literal, but only if it's not in the middle + ;; of another string or a comment. - (let* ((raw-str-regexp + (let* ((non-standard-str-regexp (rx - (seq - ;; The "r" starts the raw string. Capture it as group 1 to mark it as such syntactically: - (group "r") - - ;; Then either: - (or - ;; a sequence at least one "#" (followed by quote). Capture all - ;; but the last "#" as group 2 for this case. - (seq (group (* "#")) "#\"") - - ;; ...or a quote without any "#". Capture it as group 3. This is - ;; used later to match the opposite quote only if this capture - ;; occurred - (group "\"")) - - ;; The contents of the string: - (*? anything) - - ;; If there are any backslashes at the end of the string, capture - ;; them as group 4 so we can suppress the normal escape syntax - ;; parsing: - (group (* "\\")) - - ;; Then the end of the string--the backreferences ensure that we - ;; only match the kind of ending that corresponds to the beginning - ;; we had: - (or - ;; There were "#"s - capture the last one as group 5 to mark it as - ;; the end of the string: - (seq "\"" (backref 2) (group "#")) - - ;; No "#"s - capture the ending quote (using a backref to group 3, - ;; so that we can't match a quote if we had "#"s) as group 6 - (group (backref 3)))) - ;; If it matches, it ends up with the starting character of the string - ;; as group 1, any ending backslashes as group 4, and the ending - ;; character as either group 5 or group 6. + (or + ;; Raw string: if it matches, it ends up with the starting character + ;; of the string as group 1, any ending backslashes as group 4, and + ;; the ending character as either group 5 or group 6. + (seq + ;; The "r" starts the raw string. Capture it as group 1 to mark it as such syntactically: + (group "r") + + ;; Then either: + (or + ;; a sequence at least one "#" (followed by quote). Capture all + ;; but the last "#" as group 2 for this case. + (seq (group (* "#")) "#\"") + + ;; ...or a quote without any "#". Capture it as group 3. This is + ;; used later to match the opposite quote only if this capture + ;; occurred + (group "\"")) + + ;; The contents of the string: + (*? anything) + + ;; If there are any backslashes at the end of the string, capture + ;; them as group 4 so we can suppress the normal escape syntax + ;; parsing: + (group (* "\\")) + + ;; Then the end of the string--the backreferences ensure that we + ;; only match the kind of ending that corresponds to the beginning + ;; we had: + (or + ;; There were "#"s - capture the last one as group 5 to mark it as + ;; the end of the string: + (seq "\"" (backref 2) (group "#")) + + ;; No "#"s - capture the ending quote (using a backref to group 3, + ;; so that we can't match a quote if we had "#"s) as group 6 + (group (backref 3)))) + + ;; Character literal: match the beginning ' of a character literal + ;; as group 7, and the ending one as group 8 + (seq + (group "'") + (or + (seq + "\\" + (or + (: "U" (= 8 xdigit)) + (: "u" (= 4 xdigit)) + (: "x" (= 2 xdigit)) + (any "'nrt0\"\\"))) + (not (any "'\\")) + ) + (group "'")) + ) ))) (rust-conditional-re-search-forward - raw-str-regexp bound - (lambda () (save-excursion - (goto-char (match-beginning 0)) - (not (rust-in-str-or-cmnt))))))) + non-standard-str-regexp bound + (lambda () + (let ((pstate (syntax-ppss (match-beginning 0)))) + (not + (or + (nth 4 pstate) ;; Skip if in a comment + (and (nth 3 pstate) (wholenump (nth 8 pstate)) (< (nth 8 pstate) (match-beginning 0))) ;; Skip if in a string that isn't starting here + ))))))) (defvar rust-mode-font-lock-syntactic-keywords (append - ;; Handle single quoted character literals: - (mapcar (lambda (re) (list re '(1 "\"") '(2 "\""))) - '("\\('\\)[^']\\('\\)" - "\\('\\)\\\\['nrt\"\\]\\('\\)" - "\\('\\)\\\\x[[:xdigit:]]\\{2\\}\\('\\)" - "\\('\\)\\\\u[[:xdigit:]]\\{4\\}\\('\\)" - "\\('\\)\\\\U[[:xdigit:]]\\{8\\}\\('\\)")) - ;; Handle raw strings: - `((rust-look-for-raw-string (1 "|") (4 "_" nil t) (5 "|" nil t) (6 "|" nil t))))) + ;; Handle raw strings and character literals: + `((rust-look-for-non-standard-string (1 "|" nil t) (4 "_" nil t) (5 "|" nil t) (6 "|" nil t) (7 "\"" nil t) (8 "\"" nil t))))) (defun rust-mode-syntactic-face-function (state) "Syntactic face function to distinguish doc comments from other comments." @@ -768,7 +798,7 @@ This is written mainly to be used as `end-of-defun-function' for Rust." (setq-local indent-line-function 'rust-mode-indent-line) ;; Fonts - (add-to-list 'font-lock-extend-region-functions 'rust-extend-region-raw-string) + (add-to-list 'font-lock-extend-region-functions 'rust-font-lock-extend-region) (setq-local font-lock-defaults '(rust-mode-font-lock-keywords nil nil nil nil (font-lock-syntactic-keywords . rust-mode-font-lock-syntactic-keywords) -- cgit v1.2.3 From 52317099f160869f37d9b02e279db4ab474c26a3 Mon Sep 17 00:00:00 2001 From: Micah Chalmer Date: Mon, 8 Jun 2015 02:49:52 -0400 Subject: Speed up calls to "looking-back" --- rust-mode.el | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/rust-mode.el b/rust-mode.el index 193f672..05d8528 100644 --- a/rust-mode.el +++ b/rust-mode.el @@ -20,6 +20,27 @@ "Set variable VAR to value VAL in current buffer." (list 'set (list 'make-local-variable (list 'quote var)) val)))) +(defun rust-looking-back-str (str) + "Like `looking-back' but for fixed strings rather than regexps (so that it's not so slow)" + (let ((len (length str))) + (and (> (point) len) + (equal str (buffer-substring-no-properties (- (point) len) (point)))))) + +(defun rust-looking-back-symbols (SYMS) + "Return non-nil if the point is just after a complete symbol that is a member of the list of strings SYMS" + (save-excursion + (let* ((pt-orig (point)) + (beg-of-symbol (progn (forward-thing 'symbol -1) (point))) + (end-of-symbol (progn (forward-thing 'symbol 1) (point)))) + (and + (= end-of-symbol pt-orig) + (member (buffer-substring-no-properties beg-of-symbol pt-orig) SYMS))))) + +(defun rust-looking-back-ident () + "Non-nil if we are looking backwards at a valid rust identifier" + (let ((beg-of-symbol (save-excursion (forward-thing 'symbol -1) (point)))) + (looking-back rust-re-ident beg-of-symbol))) + ;; Syntax definitions and helpers (defvar rust-mode-syntax-table (let ((table (make-syntax-table))) @@ -77,7 +98,7 @@ (defun rust-rewind-irrelevant () (let ((starting (point))) (skip-chars-backward "[:space:]\n") - (if (looking-back "\\*/" nil) (backward-char)) + (if (rust-looking-back-str "*/") (backward-char)) (if (rust-in-str-or-cmnt) (rust-rewind-past-str-cmnt)) (if (/= starting (point)) @@ -141,13 +162,13 @@ ;; ((skip-dot-identifier (lambda () - (when (looking-back (concat "\\." rust-re-ident) nil) + (when (and (rust-looking-back-ident) (save-excursion (forward-thing 'symbol -1) (= ?. (char-before)))) (forward-thing 'symbol -1) (backward-char) (- (current-column) rust-indent-offset))))) (cond ;; foo.bar(...) - ((looking-back ")" nil) + ((rust-looking-back-str ")") (backward-list 1) (funcall skip-dot-identifier)) @@ -271,7 +292,7 @@ ;; ..or if the previous line ends with any of these: ;; { ? : ( , ; [ } ;; then we are at the beginning of an expression, so stay on the baseline... - (looking-back "[(,:;?[{}]\\|[^|]|" nil) + (looking-back "[(,:;?[{}]\\|[^|]|" (- (point) 2)) ;; or if the previous line is the end of an attribute, stay at the baseline... (progn (rust-rewind-to-beginning-of-current-level-expr) (looking-at "#"))))) baseline -- cgit v1.2.3 From 07943f0080ebd7cf3ab021f7203e3a424eccd2ce Mon Sep 17 00:00:00 2001 From: Micah Chalmer Date: Mon, 8 Jun 2015 02:59:00 -0400 Subject: Perform syntactic angle bracket matching Add angle brackets to the list of emacs matching characters. Use syntactic fontification to suppress this where it is not appropriate (in less than operators, arrows, etc.) Remove the non-syntactic version that was there previously. For electric-pair-mode, suppress the pairing for < and > characters that are not angle brackets. Because syntax is used for indentation, this fixes some problems with it, particularly when attempting to stretch type parameter lists over multiple lines. --- rust-mode-tests.el | 649 ++++++++++++++++++++++++++++++++++++++++++++++++++++- rust-mode.el | 457 ++++++++++++++++++++++++++++++------- 2 files changed, 1028 insertions(+), 78 deletions(-) diff --git a/rust-mode-tests.el b/rust-mode-tests.el index a846a27..4a6cf20 100644 --- a/rust-mode-tests.el +++ b/rust-mode-tests.el @@ -710,6 +710,7 @@ INIT-POS, FINAL-POS are position symbols found in `rust-test-positions-alist'." (with-temp-buffer (rust-mode) (insert source-code) + (font-lock-fontify-buffer) (goto-char (rust-get-buffer-pos init-pos)) (apply manip-func args) (should (equal (point) (rust-get-buffer-pos final-pos))))) @@ -723,6 +724,7 @@ All positions are position symbols found in `rust-test-positions-alist'." (with-temp-buffer (rust-mode) (insert source-code) + (font-lock-fontify-buffer) (goto-char (rust-get-buffer-pos init-pos)) (apply manip-func args) (should (equal (list (region-beginning) (region-end)) @@ -1303,7 +1305,7 @@ fn indented_already() { \n // The previous line already has its spaces } ") - + (font-lock-fontify-buffer) (goto-line 11) (move-to-column 0) (indent-for-tab-command) @@ -1483,6 +1485,14 @@ la la\"); ;; Needs to leave 1 space before "world" "\"hello \\\n world\"")) +(ert-deftest indent-multi-line-type-param-list () + (test-indent + " +pub fn foo() { + hello(); +}")) + (defun rust-test-matching-parens (content pairs &optional nonparen-positions) "Assert that in rust-mode, given a buffer with the given `content', emacs's paren matching will find all of the pairs of positions @@ -1562,3 +1572,640 @@ la la\"); "r#\"\"\"#;\n'q'" '("r#\"\"\"#" font-lock-string-face "'q'" font-lock-string-face))) + +(ert-deftest rust-test-basic-paren-matching () + (rust-test-matching-parens + " +fn foo() { + let a = [1, 2, 3]; +}" + '((8 9) ;; Parens of foo() + (11 36) ;; Curly braces + (25 33) ;; Square brackets + ))) + +(ert-deftest rust-test-paren-matching-generic-fn () + (rust-test-matching-parens + " +fn foo() { +}" + '((8 10) ;; Angle brackets + (11 12) ;; Parens + (14 16) ;; Curly braces + ))) + +(ert-deftest rust-test-paren-matching-generic-fn-with-return-value () + (rust-test-matching-parens + " +fn foo() -> bool { + false +}" + '((8 10) ;; Angle brackets + (11 12) ;; Parens + (22 34) ;; Curly braces + ) + + '(15 ;; The ">" in "->" is not an angle bracket + ))) + +(ert-deftest rust-test-paren-matching-match-stmt () + (rust-test-matching-parens + " +fn foo() { + something_str(match ::method() { + Some(_) => \"Got some\", + None => \"Nada\" + }); +}" + '((8 9) ;; parens of fn foo + (11 127) ;; curly braces of foo + (30 124) ;; parens of something_str + (37 51) ;; angle brackets of + (60 61) ;; parens of method() + (63 123) ;; curly braces of match + (77 79) ;; parens of Some(_) + ) + + '(82 ;; > in first => + 112 ;; > in second => + ))) + +(ert-deftest rust-test-paren-matching-bitshift-operators () + (rust-test-matching-parens + " +fn foo(z:i32) { + let a:Option> = Some(Ok(4 >> 1)); + let b = a.map(|x| x.map(|y| y << 3)); + let trick_question = z<<::method(); // First two + ) + '(64 ;; The >> inside Some(Ok()) are not angle brackets + 65 ;; The >> inside Some(Ok()) are not angle brackets + 106 ;; The << inside map() are not angle brackets + 107 ;; The << inside map() are not angle brackets + 140 ;; The << before are not angle brackets + 141 ;; The << before are not angle brackets + 183 ;; The < inside the comment + ))) + +(ert-deftest rust-test-paren-matching-angle-bracket-after-colon-ident () + (rust-test-matching-parens + " +struct Bla { + a:Option<(i32,Option)>, + b:Option, + c:bool +} + +fn f(x:i32,y:Option) { + let z:Option = None; + let b:Bla = Bla{ + a:None, + b:None, + c:x + (30 49) ;; Outer angle brackets of a:Option<...> + (42 47) ;; Inner angle brackets of Option + (64 66) ;; Angle brackets of Option + (102 106) ;; Angle brackets of y:Option + (127 131) ;; Angle brackets of z:Option + (154 157) ;; Angle brackets of b:Bla + ) + '(209 ;; less than operator in c:x Bar { + Bar { + b:x<3 + } +}" + '() + '(17 ;; the -> is not a brace + 46 ;; x<3 the < is a less than sign + )) + ) + +(ert-deftest rust-test-paren-matching-nested-struct-literals () + (rust-test-matching-parens + " +fn f(x:i32,y:i32) -> Foo { + Foo{ + bar:Bar{ + a:3, + b:x + ) + '(92 ;; less than operator x X>() -> Z { +} +" + '((8 23) ;; The angle brackets of foo + (20 22 ;; The angle brackets of X + )) + '(17 ;; The first -> + 28 ;; The second -> + ) + )) + +(ert-deftest rust-test-paren-matching-lt-ops-in-fn-params () + (rust-test-matching-parens + " +fn foo(x:i32) { + f(x < 3); +} +" + '() + '(26 ;; The < inside f is a less than operator + ) + )) + +(ert-deftest rust-test-paren-matching-lt-ops-in-fn-params () + (rust-test-matching-parens + " +fn foo(x:i32) -> bool { + return x < 3; +} +" + '() + '(17 ;; The -> + 39 ;; The < after return is a less than operator + ) + )) + +(ert-deftest rust-test-type-paren-matching-angle-brackets-in-type-items () + (rust-test-matching-parens + " +type Foo = Blah; +type Bar = (Foo, Bletch); +type ThisThing = HereYouGo B>,E>>;" + '((17 21) ;; Angle brackets of Blah + (32 34) ;; Angle brackets of Bar + (50 52) ;; Angle brackets of Bletch + (70 78) ;; Angle brackets of ThisThing + (91 118) ;; Angle brackets of HereYouGo<...> + (95 117) ;; Angle brackets of Y + (106 111) ;; Angle brackets of B> + (108 110) ;; Angle brackets of C + (114 116) ;; Angle brackets of E + ))) + +(ert-deftest rust-test-paren-matching-tuple-like-struct () + (rust-test-matching-parens + " +struct A(Option); +struct C(Result);" + '((17 19) ;; The angle brackets + (10 20) ;; The parens of A(); + (31 33) ;; The angle brackets of C + (41 47) ;; The angle brackets of Result + ) + '())) + +(ert-deftest rust-test-paren-matching-in-enum () + (rust-test-matching-parens + " +enum Boo { + TupleLike(Option), + StructLike{foo: Result} +}" + '((10 12) ;; Angle brackets of Boo + (36 38) ;; Angle brackets of Option + (68 74) ;; Angle brackets of Result + ))) + +(ert-deftest rust-test-paren-matching-assoc-type-bounds () + (rust-test-matching-parens + "impl >> Thing {}" + '((6 29) ;; Outer angle brackets of impl + (10 28) ;; Outer angle brackets of B> + (24 26) ;; Inner angle brackets of C + (36 38) ;; Angle brackets of Thing + ) + )) + +(ert-deftest rust-test-paren-matching-plus-signs-in-expressions-and-bounds () + ;; Note that as I write this, the function "bluh" below does not compile, but + ;; it warns that the equality constraint in a where clause is "not yet + ;; supported." It seems that the compiler will support this eventually, so + ;; the emacs mode needs to support it. + (rust-test-matching-parens + "fn foo,B>(a:A,b:B) -> bool where B:Trait3+Trait4 { + 2 + a < 3 && 3 + b > 11 +} + +fn bluh() where A:Fn()+MyTrait, MyTrait::AssocType = Option { +} + +fn fluh() where C:Fn(i32) -> (i32, i32) + SomeTrait, C::AssocType = OtherThing { +}" + '((7 30) ;; Angle brackets of foo<...> + (23 27) ;; Angle brackets of Trait2 + (63 67) ;; Angle brackets of Trait3 + (75 79) ;; Angle brackets of Trait4 + + (121 123) ;; Angle brackets of bluh + (147 151) ;; Angle brackets of MyTrait + + (161 163) ;; Angle brackets of MyTrait + (184 189) ;; Angle brackets of Option + + (203 205) ;; Angle brackets of + (250 254) ;; Angle brackets of SomeTrait + (282 287) ;; Angle brackets of Option + ) + '(93 ;; Less-than sign of a < 3 + 106 ;; Greater than sign of b > 11 + ))) + +(ert-deftest rust-test-paren-matching-generic-type-in-tuple-return-type () + (rust-test-matching-parens + "pub fn take(mut self) -> (EmptyBucket, K, V) {}" + '((38 46)) + )) + +(ert-deftest rust-test-paren-matching-references-and-logical-and () + (rust-test-matching-parens + " +fn ampersand_check(a: &Option, b:bool) -> &Option { + a.map(|x| { + b && x < 32 + }) +}" + '((31 35) ;; Option + (56 60) ;; Option + ) + '(95 ;; x < 32 + ) + ) + ) + +(ert-deftest rust-test-paren-matching-lt-sign-in-if-statement () + (rust-test-matching-parens + " +fn if_check(a:i32,b:i32,c:i32) { + if a + b < c { + + } + if a < b { + + } + if (c < a) { + + } +} + +fn while_check(x:i32,y:i32) -> bool { + while x < y { + } + for x in y < x { + } + match y < z { + true => (), _ => () + } + return z < y; +}" + '() + '(48 ;; b < c + 78 ;; a < b + 109 ;; (c < a) + + 184 ;; x < y + 211 ;; y < x + 235 ;; y < z + 288 ;; z < y + ))) + +(ert-deftest rust-test-paren-matching-lt-expr-with-field () + (rust-test-matching-parens + "fn foo() { x.y < 3 }" + '() + '(16 ;; x.y < 3 + ))) + +(ert-deftest rust-test-paren-matching-lt-expr-with-quote () + (rust-test-matching-parens + " +fn quote_check() { + 'x' < y; + \"y\" < x; + r##\"z\"## < q; + a <= 3 && b < '2' +}" + '() + '(29 ;; 'x' < y + 42 ;; "y" < x + 60 ;; r##"z"## < q + 71 ;; a <= '3' + 81 ;; b < '2' + ))) + +(ert-deftest rust-test-paren-matching-keywords-capitalized-are-ok-type-names () + (rust-test-matching-parens + " +fn foo() -> Box { + let z:If = If(a < 3); +}" + '((17 21) ;; Box + (37 42) ;; If + ) + '(51 ;; If(a < 3) + ))) + +(ert-deftest rust-test-paren-matching-lt-expression-inside-macro () + (rust-test-matching-parens + "fn bla() { assert!(x < y); }" + '() + '(22 ;; x < y + ))) + +(ert-deftest rust-test-paren-matching-array-types-with-generics () + (rust-test-matching-parens + "fn boo () -> [Option] {}" + '((21 25)))) + +(ert-deftest rust-test-paren-matching-angle-bracket-inner-reference () + (rust-test-matching-parens + "fn x() -> Option<&Node> {}" + '((17 26) ;; Option + (23 25) ;; Node + ))) + +(ert-deftest rust-test-paren-matching-lt-operator-after-semicolon () + (rust-test-matching-parens + "fn f(x:i32) -> bool { (); x < 3 }" + '() + '(29 + ))) + +(ert-deftest rust-test-paren-matching-lt-operator-after-comma () + (rust-test-matching-parens + "fn foo() { + (e, a < b) +}" + '((16 25) ;; The parens () + ) + '(22 ;; The < operator + ))) + +(ert-deftest rust-test-paren-matching-lt-operator-after-let () + (rust-test-matching-parens + "fn main() { + let x = a < b; +}" + '((11 32) ;; The { } + ) + '(27 ;; The < operator + ))) + +(ert-deftest rust-test-paren-matching-two-lt-ops-in-a-row () + (rust-test-matching-parens + "fn next(&mut self) -> Option<::Item>" + '((29 51) ;; Outer Option<> + (30 44) ;; Inner + ) + '(21 + ))) + +(ert-deftest rust-test-paren-matching-lt-after-caret () + (rust-test-matching-parens + "fn foo() { x^2 < 3 }" + '((10 20) ;; The { } + ) + '(16 ;; The < operator + ))) + +(ert-deftest rust-test-paren-matching-lt-operator-after-special-type () + (rust-test-matching-parens + "fn foo() { low as uint <= c }" + '((10 29)) + '(24))) + +(ert-deftest rust-test-paren-matching-lt-operator-after-closing-curly-brace () + (rust-test-matching-parens + "fn main() { if true {} a < 3 }" + '((11 30) + ) + '(26))) + +(ert-deftest rust-test-paren-matching-const () + (rust-test-matching-parens + " +const BLA = 1 << 3; +const BLUB = 2 < 4;" + '() + '(16 + 17 ;; Both chars of the << in 1 << 3 + 37 ;; The < in 2 < 4 + ))) + +(ert-deftest rust-test-paren-matching-c-like-enum () + (rust-test-matching-parens + " +enum CLikeEnum { + Two = 1 << 1, + Four = 1 << 2 +}" + '((17 56 ;; The { } of the enum + )) + '(31 + 32 ;; The first << + 50 + 51 ;; The second << + ))) + +(ert-deftest rust-test-paren-matching-no-angle-brackets-in-macros () + (rust-test-matching-parens + " +fn foo(a:A) { + macro_a!( foo:: ); + macro_b![ foo as Option ]; +} + +macro_c!{ + struct Boo {} +}" + '((8 10)) + ;; Inside macros, it should not find any angle brackets, even if it normally + ;; would + '(38 ;; macro_a < + 57 ;; macro_a > + 89 ;; macro_b < + 91 ;; macro_b > + 123 ;; macro_c < + 125 ;; macro_d > + ))) + +(ert-deftest rust-test-paren-matching-type-with-module-name () + (rust-test-matching-parens + " +const X: libc::c_int = 1 << 2; +fn main() { + let z: libc::c_uint = 1 << 4; +} +" + '((43 79)) ;; The curly braces + '(27 + 28 ;; The first << + 73 + 74 ;; The second << + ))) + +(ert-deftest rust-test-paren-matching-qualififed-struct-literal () + (rust-test-matching-parens + " +fn foo() -> Fn(asd) -> F { + let z = foo::Struct{ b: 1 << 4, c: 2 < 4 } +}" + '((30 80) ;; Outer curly brackets + ) + '(62 + 63 ;; The shift operator + 73 ;; The less than operator + ))) + +(ert-deftest rust-test-paren-matching-let-mut () + (rust-test-matching-parens + " +fn f() { + let mut b = 1 < 3; + let mut i = 1 << 3; +} +" + '() + '(28 ;; 1 < 3 + 51 + 52 ;; 1 << 3 + ))) + +(ert-deftest rust-test-paren-matching-as-ref-type () + (rust-test-matching-parens + "fn f() { + let a = b as &Foo; +}" + '((31 35) ;; Angle brackets Foo + ))) + +(ert-deftest rust-test-paren-matching-type-ascription () + (rust-test-matching-parens + " +fn rfc803() { + let z = a < b:FunnkyThing; + let s = Foo { + a: b < 3, + b: d:CrazyStuff < 3, + c: 2 < x:CrazyStuff + } +}" + '((45 49) ;; FunkyThing + (111 115) ;; CrazyStuff + (149 154) ;; CrazyStuff + ) + '(30 ;; a < b + 83 ;; b < 3 + 117 ;; d... < 3 + 135 ;; 2 < x + ))) + +(ert-deftest rust-test-paren-matching-angle-brackets-in-enum-with-where-claause () + (rust-test-matching-parens + " +enum MyEnum where T:std::fmt::Debug { + Thing(Option) +}" + '((13 15) ;; MyEnum + (59 61) ;; Option + ))) + +(ert-deftest rust-test-paren-matching-where-clauses-with-closure-types () + (rust-test-matching-parens + " +enum Boo<'a,T> where T:Fn() -> Option<&'a str> + 'a { + Thingy(Option<&'a T>) +} + +fn foo<'a>() -> Result where C::X: D, B:FnMut() -> Option+'a { + Foo(a < b) +} + +type Foo where T: Copy = Box; +" + '((10 15) ;; Boo<'a,T> + (39 47) ;; Option<&'a str> + (72 78) ;; Option<&'a T> + + (106 110) ;; Result + (125 127) ;; D + (149 151) ;; Option + (184 186) ;; Foo + (207 209) ;; Box + ) + + '(168 ;; Foo(a < b) + ) + )) + +(ert-deftest rust-test-angle-bracket-matching-turned-off () + (let ((rust-match-angle-brackets nil)) + (rust-test-matching-parens + "fn foo() {}" + '((10 11)) + '(7 9)))) + +;; If electric-pair-mode is available, load it and run the tests that use it. If not, +;; no error--the tests will be skipped. +(require 'elec-pair nil t) + +;; The emacs 23 version of ERT does not have test skipping functionality. So +;; don't even define these tests if elec-pair is not available. +(when (featurep 'elec-pair) + (defun test-electric-pair-insert (original point-pos char closer) + (let ((old-electric-pair-mode electric-pair-mode)) + (electric-pair-mode 1) + (unwind-protect + (with-temp-buffer + (rust-mode) + (insert original) + (font-lock-fontify-buffer) + + (goto-char point-pos) + (deactivate-mark) + (let ((last-command-event char)) (self-insert-command 1)) + (should (equal (char-after) + (or closer (aref original point-pos))))) + (electric-pair-mode (or old-electric-pair-mode 1))))) + + (ert-deftest rust-test-electric-pair-generic-fn () + (test-electric-pair-insert "fn foo() { }" 7 ?< ?>)) + + (ert-deftest rust-test-electric-pair-impl-param () + (test-electric-pair-insert "impl Foo for Bar" 5 ?< ?>)) + + (ert-deftest rust-test-electric-pair-impl-for-type-param () + (test-electric-pair-insert "impl Foo for Bar" 22 ?< ?>)) + + (ert-deftest rust-test-electric-pair-lt-expression () + (test-electric-pair-insert "fn foo(bar:i32) -> bool { bar }" 30 ?< nil)) + + (ert-deftest rust-test-electric-pair-lt-expression-in-struct-literal () + (test-electric-pair-insert "fn foo(x:i32) -> Bar { Bar { a:(bleh() + whatever::()), b:x } }" 63 ?< nil)) + + (ert-deftest rust-test-electric-pair-lt-expression-capitalized-keyword () + (test-electric-pair-insert "fn foo() -> Box" 16 ?< ?>)) + ) diff --git a/rust-mode.el b/rust-mode.el index 05d8528..d890299 100644 --- a/rust-mode.el +++ b/rust-mode.el @@ -41,6 +41,10 @@ (let ((beg-of-symbol (save-excursion (forward-thing 'symbol -1) (point)))) (looking-back rust-re-ident beg-of-symbol))) +(defun rust-looking-back-macro () + "Non-nil if looking back at an ident followed by a !" + (save-excursion (backward-char) (and (= ?! (char-after)) (rust-looking-back-ident)))) + ;; Syntax definitions and helpers (defvar rust-mode-syntax-table (let ((table (make-syntax-table))) @@ -53,6 +57,11 @@ (modify-syntax-entry ?\" "\"" table) (modify-syntax-entry ?\\ "\\" table) + ;; Angle brackets. We suppress this with syntactic fontification when + ;; needed + (modify-syntax-entry ?< "(>" table) + (modify-syntax-entry ?> ")<" table) + ;; Comments (modify-syntax-entry ?/ ". 124b" table) (modify-syntax-entry ?* ". 23" table) @@ -92,6 +101,13 @@ :type 'string :group 'rust-mode) +(defcustom rust-match-angle-brackets t + "Enable angle bracket matching. Attempt to match `<' and `>' where + appropriate." + :type 'boolean + :safe #'booleanp + :group 'rust-mode) + (defun rust-paren-level () (nth 0 (syntax-ppss))) (defun rust-in-str-or-cmnt () (nth 8 (syntax-ppss))) (defun rust-rewind-past-str-cmnt () (goto-char (nth 8 (syntax-ppss)))) @@ -103,6 +119,15 @@ (rust-rewind-past-str-cmnt)) (if (/= starting (point)) (rust-rewind-irrelevant)))) +(defun rust-in-macro () + (save-excursion + (when (> (rust-paren-level) 0) + (backward-up-list) + (rust-rewind-irrelevant) + (or (rust-looking-back-macro) + (and (rust-looking-back-ident) (save-excursion (backward-sexp) (rust-rewind-irrelevant) (rust-looking-back-str "macro_rules!"))) + (rust-in-macro)) + ))) (defun rust-align-to-expr-after-brace () (save-excursion @@ -342,6 +367,7 @@ "str" "char")) (defconst rust-re-CamelCase "[[:upper:]][[:word:][:multibyte:]_[:digit:]]*") +(defconst rust-re-pre-expression-operators "[-=!%&*/:<>[{(|.^;}]") (defun rust-re-word (inner) (concat "\\<" inner "\\>")) (defun rust-re-grab (inner) (concat "\\(" inner "\\)")) (defun rust-re-grabword (inner) (rust-re-grab (rust-re-word inner))) @@ -545,10 +571,362 @@ (and (nth 3 pstate) (wholenump (nth 8 pstate)) (< (nth 8 pstate) (match-beginning 0))) ;; Skip if in a string that isn't starting here ))))))) +(defun rust-syntax-class-before-point () + (when (> (point) 1) + (syntax-class (syntax-after (1- (point)))))) + +(defun rust-rewind-qualified-ident () + (while (rust-looking-back-ident) + (backward-sexp) + (when (save-excursion (rust-rewind-irrelevant) (rust-looking-back-str "::")) + (rust-rewind-irrelevant) + (backward-char 2) + (rust-rewind-irrelevant)))) + +(defun rust-rewind-type-param-list () + (cond + ((and (rust-looking-back-str ">") (equal 5 (rust-syntax-class-before-point))) + (backward-sexp) + (rust-rewind-irrelevant)) + + ;; We need to be able to back up past the Fn(args) -> RT form as well. If + ;; we're looking back at this, we want to end up just after "Fn". + ((member (char-before) '(?\] ?\) )) + (let* ((is-paren (rust-looking-back-str ")")) + (dest (save-excursion + (backward-sexp) + (rust-rewind-irrelevant) + (or + (when (rust-looking-back-str "->") + (backward-char 2) + (rust-rewind-irrelevant) + (when (rust-looking-back-str ")") + (backward-sexp) + (point))) + (and is-paren (point)))))) + (when dest + (goto-char dest)))))) + +(defun rust-rewind-to-decl-name () + "If we are before an ident that is part of a declaration that + can have a where clause, rewind back to just before the name of + the subject of that where clause and return the new point. + Otherwise return nil" + + (let* ((ident-pos (point)) + (newpos (save-excursion + (rust-rewind-irrelevant) + (rust-rewind-type-param-list) + (cond + ((rust-looking-back-symbols '("fn" "trait" "enum" "struct" "impl" "type")) ident-pos) + + ((equal 5 (rust-syntax-class-before-point)) + (backward-sexp) + (rust-rewind-to-decl-name)) + + ((looking-back "[:,'+=]" (1- (point))) + (backward-char) + (rust-rewind-to-decl-name)) + + ((rust-looking-back-str "->") + (backward-char 2) + (rust-rewind-to-decl-name)) + + ((rust-looking-back-ident) + (rust-rewind-qualified-ident) + (rust-rewind-to-decl-name)))))) + (when newpos (goto-char newpos)) + newpos)) + +(defun rust-is-in-expression-context (token) + "Return t if what comes right after the point is part of an + expression (as opposed to starting a type) by looking at what + comes before. Takes a symbol that roughly indicates what is + after the point. + + This function is used as part of `rust-is-lt-char-operator' as + part of angle bracket matching, and is not intended to be used + outside of this context." + + (save-excursion + (let ((postchar (char-after))) + (rust-rewind-irrelevant) + + ;; A type alias or ascription could have a type param list. Skip backwards past it. + (when (member token '(ambiguous-operator open-brace)) + (rust-rewind-type-param-list)) + + (cond + + ;; Certain keywords always introduce expressions + ((rust-looking-back-symbols '("if" "while" "match" "return" "box" "in")) t) + + ;; "as" introduces a type + ((rust-looking-back-symbols '("as")) nil) + + ;; An open angle bracket never introduces expression context WITHIN the angle brackets + ((and (equal token 'open-brace) (equal postchar ?<)) nil) + + ;; An ident! followed by an open brace is a macro invocation. Consider + ;; it to be an expression. + ((and (equal token 'open-brace) (rust-looking-back-macro)) t) + + ;; An identifier is right after an ending paren, bracket, angle bracket + ;; or curly brace. It's a type if the last sexp was a type. + ((and (equal token 'ident) (equal 5 (rust-syntax-class-before-point))) + (backward-sexp) + (rust-is-in-expression-context 'open-brace)) + + ;; If a "for" appears without a ; or { before it, it's part of an + ;; "impl X for y", so the y is a type. Otherwise it's + ;; introducing a loop, so the y is an expression + ((and (equal token 'ident) (rust-looking-back-symbols '("for"))) + (backward-sexp) + (rust-rewind-irrelevant) + (looking-back "[{;]" (1- (point)))) + + ((rust-looking-back-ident) + (rust-rewind-qualified-ident) + (rust-rewind-irrelevant) + (cond + ((equal token 'open-brace) + ;; We now know we have: + ;; ident [{([] + ;; where [{([] denotes either a {, ( or [. This character is bound as postchar. + (cond + ;; If postchar is a paren or square bracket, then if the brace is a type if the identifier is one + ((member postchar '(?\( ?\[ )) (rust-is-in-expression-context 'ident)) + + ;; If postchar is a curly brace, the brace can only be a type if + ;; ident2 is the name of an enum, struct or trait being declared. + ;; Note that if there is a -> before the ident then the ident would + ;; be a type but the { is not. + ((equal ?{ postchar) + (not (and (rust-rewind-to-decl-name) + (progn + (rust-rewind-irrelevant) + (rust-looking-back-symbols '("enum" "struct" "trait" "type")))))) + )) + + ((equal token 'ambiguous-operator) + (cond + ;; An ampersand after an ident has to be an operator rather than a & at the beginning of a ref type + ((equal postchar ?&) t) + + ;; A : followed by a type then an = introduces an expression (unless it is part of a where clause of a "type" declaration) + ((and (equal postchar ?=) + (looking-back "[^:]:" (- (point) 2)) + (not (save-excursion (and (rust-rewind-to-decl-name) (progn (rust-rewind-irrelevant) (rust-looking-back-symbols '("type")))))))) + + ;; "let ident =" introduces an expression--and so does "const" and "mut" + ((and (equal postchar ?=) (rust-looking-back-symbols '("let" "const" "mut"))) t) + + ;; As a specific special case, see if this is the = in this situation: + ;; enum EnumName { Ident = + ;; In this case, this is a c-like enum and despite Ident + ;; representing a type, what comes after the = is an expression + ((and + (> (rust-paren-level) 0) + (save-excursion + (backward-up-list) + (rust-rewind-irrelevant) + (rust-rewind-type-param-list) + (and + (rust-looking-back-ident) + (progn + (rust-rewind-qualified-ident) + (rust-rewind-irrelevant) + (rust-looking-back-str "enum"))))) + t) + + ;; Otherwise the ambiguous operator is a type if the identifier is a type + ((rust-is-in-expression-context 'ident) t))) + + ((equal token 'colon) + (cond + ;; If we see a ident: not inside any braces/parens, we're at top level. + ;; There are no allowed expressions after colons there, just types. + ((<= (rust-paren-level) 0) nil) + + ;; We see ident: inside a list + ((looking-back "[{,]" (1- (point))) + (backward-up-list) + + ;; If a : appears whose surrounding paren/brackets/braces are + ;; anything other than curly braces, it can't be a field + ;; initializer and must be denoting a type. + (when (looking-at "{") + (rust-rewind-irrelevant) + (rust-rewind-type-param-list) + (when (rust-looking-back-ident) + ;; We have a context that looks like this: + ;; ident2 { [maybe paren-balanced code ending in comma] ident1: + ;; the point is sitting just after ident2, and we trying to + ;; figure out if the colon introduces an expression or a type. + ;; The answer is that ident1 is a field name, and what comes + ;; after the colon is an expression, if ident2 is an + ;; expression. + (rust-rewind-qualified-ident) + (rust-is-in-expression-context 'ident)))) + + + ;; Otherwise, if the ident: appeared with anything other than , or { + ;; before it, it can't be part of a struct initializer and therefore + ;; must be denoting a type. + nil + )) + )) + + ;; An operator-like character after a string is indeed an operator + ((and (equal token 'ambiguous-operator) + (member (rust-syntax-class-before-point) '(5 7 15))) t) + + ;; A colon that has something other than an identifier before it is a + ;; type ascription + ((equal token 'colon) nil) + + ;; A :: introduces a type (or module, but not an expression in any case) + ((rust-looking-back-str "::") nil) + + ((rust-looking-back-str ":") + (backward-char) + (rust-is-in-expression-context 'colon)) + + ;; A -> introduces a type + ((rust-looking-back-str "->") nil) + + ;; If we are up against the beginning of a list, or after a comma inside + ;; of one, back up out of it and check what the list itself is + ((or + (equal 4 (rust-syntax-class-before-point)) + (rust-looking-back-str ",")) + (backward-up-list) + (rust-is-in-expression-context 'open-brace)) + + ;; A => introduces an expression + ((rust-looking-back-str "=>") t) + + ;; A == introduces an expression + ((rust-looking-back-str "==") t) + + ;; These operators can introduce expressions or types + ((looking-back "[-+=!?&*]" (1- (point))) + (backward-char) + (rust-is-in-expression-context 'ambiguous-operator)) + + ;; These operators always introduce expressions. (Note that if this + ;; regexp finds a < it must not be an angle bracket, or it'd + ;; have been caught in the syntax-class check above instead of this.) + ((looking-back rust-re-pre-expression-operators (1- (point))) t) + )))) + +(defun rust-is-lt-char-operator () + "Return t if the < sign just after point is an operator rather + than an opening angle bracket, otherwise nil." + + (let ((case-fold-search nil)) + (save-excursion + (rust-rewind-irrelevant) + ;; We are now just after the character syntactically before the <. + (cond + + ;; If we are looking back at a < that is not an angle bracket (but not + ;; two of them) then this is the second < in a bit shift operator + ((and (rust-looking-back-str "<") + (not (equal 4 (rust-syntax-class-before-point))) + (not (rust-looking-back-str "<<")))) + + ;; On the other hand, if we are after a closing paren/brace/bracket it + ;; can only be an operator, not an angle bracket. Likewise, if we are + ;; after a string it's an operator. (The string case could actually be + ;; valid in rust for character literals.) + ((member (rust-syntax-class-before-point) '(5 7 15)) t) + + ;; If we are looking back at an operator, we know that we are at + ;; the beginning of an expression, and thus it has to be an angle + ;; bracket (starting a "::" construct.) + ((looking-back rust-re-pre-expression-operators (1- (point))) nil) + + ;; If we are looking back at a keyword, it's an angle bracket + ;; unless that keyword is "self", "true" or "false" + ((rust-looking-back-symbols rust-mode-keywords) + (rust-looking-back-symbols '("self" "true" "false"))) + + ;; If we're looking back at an identifier, this depends on whether + ;; the identifier is part of an expression or a type + ((rust-looking-back-ident) + (backward-sexp) + (or + ;; The special types can't take type param lists, so a < after one is + ;; always an operator + (looking-at (regexp-opt rust-special-types 'symbols)) + + (rust-is-in-expression-context 'ident))) + + ;; Otherwise, assume it's an angle bracket + )))) + +(defun rust-electric-pair-inhibit-predicate-wrap (char) + "Wraps the default `electric-pair-inhibit-predicate' to prevent + inserting a \"matching\" > after a < that would be treated as a + less than sign rather than as an opening angle bracket." + (or + (when (= ?< char) + (save-excursion + (backward-char) + (rust-is-lt-char-operator))) + (funcall (default-value 'electric-pair-inhibit-predicate) char))) + +(defun rust-look-for-non-angle-bracket-lt-gt (bound) + "Find an angle bracket (\"<\" or \">\") that should be part of + a matched pair Relies on the fact that when it finds a < or >, + we have already decided which previous ones are angle brackets + and which ones are not. So this only really works as a + font-lock-syntactic-keywords matcher--it won't work at + arbitrary positions without the earlier parts of the buffer + having already been covered." + + (rust-conditional-re-search-forward + "[<>]" bound + (lambda () + (goto-char (match-beginning 0)) + (cond + ;; If matching is turned off suppress all of them + ((not rust-match-angle-brackets) t) + + ;; We don't take < or > in strings or comments to be angle brackets + ((rust-in-str-or-cmnt) t) + + ;; Inside a macro we don't really know the syntax. Any < or > may be an + ;; angle bracket or it may not. But we know that the other braces have + ;; to balance regardless of the < and >, so if we don't treat any < or > + ;; as angle brackets it won't mess up any paren balancing. + ((rust-in-macro) t) + + ((looking-at "<") + (rust-is-lt-char-operator)) + + ((looking-at ">") + (cond + ;; Don't treat the > in -> or => as an angle bracket + ((member (char-before (point)) '(?- ?=)) t) + + ;; If we are at top level and not in any list, it can't be a closing + ;; angle bracket + ((>= 0 (rust-paren-level)) t) + + ;; Otherwise, treat the > as a closing angle bracket if it would + ;; match an opening one + ((save-excursion + (backward-up-list) + (not (looking-at "<")))))))))) + (defvar rust-mode-font-lock-syntactic-keywords (append ;; Handle raw strings and character literals: - `((rust-look-for-non-standard-string (1 "|" nil t) (4 "_" nil t) (5 "|" nil t) (6 "|" nil t) (7 "\"" nil t) (8 "\"" nil t))))) + `((rust-look-for-non-standard-string (1 "|" nil t) (4 "_" nil t) (5 "|" nil t) (6 "|" nil t) (7 "\"" nil t) (8 "\"" nil t))) + ;; Find where < and > characters represent operators rather than angle brackets: + '((rust-look-for-non-angle-bracket-lt-gt (0 "." t))))) (defun rust-mode-syntactic-face-function (state) "Syntactic face function to distinguish doc comments from other comments." @@ -730,81 +1108,6 @@ 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)))) -;; Angle-bracket matching. This is kind of a hack designed to deal -;; with the fact that we can't add angle-brackets to the list of -;; matching characters unconditionally. Basically we just have some -;; special-case code such that whenever `>` is typed, we look -;; backwards to find a matching `<` and highlight it, whether or not -;; this is *actually* appropriate. This could be annoying so it is -;; configurable (but on by default because it's awesome). - -(defcustom rust-blink-matching-angle-brackets t - "Blink matching `<` (if any) when `>` is typed" - :type 'boolean - :group 'rust-mode) - -(defvar rust-point-before-matching-angle-bracket 0) - -(defvar rust-matching-angle-bracker-timer nil) - -(defun rust-find-matching-angle-bracket () - (save-excursion - (let ((angle-brackets 1) - (start-point (point)) - (invalid nil)) - (while (and - ;; didn't find a match - (> angle-brackets 0) - ;; we have no guarantee of a match, so give up eventually - (or (not blink-matching-paren-distance) - (< (- start-point (point)) blink-matching-paren-distance)) - ;; didn't hit the top of the buffer - (> (point) (point-min)) - ;; didn't hit something else weird like a `;` - (not invalid)) - (backward-char 1) - (cond - ((looking-at ">") - (setq angle-brackets (+ angle-brackets 1))) - ((looking-at "<") - (setq angle-brackets (- angle-brackets 1))) - ((looking-at "[;{]") - (setq invalid t)))) - (cond - ((= angle-brackets 0) (point)) - (t nil))))) - -(defun rust-restore-point-after-angle-bracket () - (goto-char rust-point-before-matching-angle-bracket) - (when rust-matching-angle-bracker-timer - (cancel-timer rust-matching-angle-bracker-timer)) - (setq rust-matching-angle-bracker-timer nil) - (remove-hook 'pre-command-hook 'rust-restore-point-after-angle-bracket)) - -(defun rust-match-angle-bracket-hook () - "If the most recently inserted character is a `>`, briefly moves point to matching `<` (if any)." - (interactive) - (when (and rust-blink-matching-angle-brackets - (looking-back ">" nil)) - (let ((matching-angle-bracket-point (save-excursion - (backward-char 1) - (rust-find-matching-angle-bracket)))) - (when matching-angle-bracket-point - (progn - (setq rust-point-before-matching-angle-bracket (point)) - (goto-char matching-angle-bracket-point) - (add-hook 'pre-command-hook 'rust-restore-point-after-angle-bracket) - (setq rust-matching-angle-bracker-timer - (run-at-time blink-matching-delay nil 'rust-restore-point-after-angle-bracket))))))) - -(defun rust-match-angle-bracket () - "The point should be placed on a `>`. Finds the matching `<` and moves point there." - (interactive) - (let ((matching-angle-bracket-point (rust-find-matching-angle-bracket))) - (if matching-angle-bracket-point - (goto-char matching-angle-bracket-point) - (message "no matching angle bracket found")))) - ;; For compatibility with Emacs < 24, derive conditionally (defalias 'rust-parent-mode (if (fboundp 'prog-mode) 'prog-mode 'fundamental-mode)) @@ -847,7 +1150,7 @@ This is written mainly to be used as `end-of-defun-function' for Rust." (setq-local beginning-of-defun-function 'rust-beginning-of-defun) (setq-local end-of-defun-function 'rust-end-of-defun) (setq-local parse-sexp-lookup-properties t) - (add-hook 'post-self-insert-hook 'rust-match-angle-bracket-hook)) + (setq-local electric-pair-inhibit-predicate 'rust-electric-pair-inhibit-predicate-wrap)) ;;;###autoload (add-to-list 'auto-mode-alist '("\\.rs\\'" . rust-mode)) -- cgit v1.2.3