diff --git a/rust-mode-tests.el b/rust-mode-tests.el index 2b187282..2e6f77f1 100644 --- a/rust-mode-tests.el +++ b/rust-mode-tests.el @@ -919,3 +919,89 @@ list of substrings of `STR' each followed by its face." "main" font-lock-function-name-face "let" font-lock-keyword-face "'\\''" font-lock-string-face))) + +(ert-deftest indent-method-chains-no-align () + (let ((rust-indent-method-chain nil)) (test-indent + " +fn main() { + let x = thing.do_it() + .aligned() + .more_alignment(); +} +" + ))) + +(ert-deftest indent-method-chains-with-align () + (let ((rust-indent-method-chain t)) (test-indent + " +fn main() { + let x = thing.do_it() + .aligned() + .more_alignment(); +} +" + ))) + +(ert-deftest indent-method-chains-with-align-and-second-stmt () + (let ((rust-indent-method-chain t)) (test-indent + " +fn main() { + let x = thing.do_it() + .aligned() + .more_alignment(); + foo.bar(); +} +" + ))) + +(ert-deftest indent-method-chains-field () + (let ((rust-indent-method-chain t)) (test-indent + " +fn main() { + let x = thing.do_it + .aligned + .more_alignment(); +} +" + ))) + +(ert-deftest indent-method-chains-double-field-on-first-line () + (let ((rust-indent-method-chain t)) (test-indent + " +fn main() { + let x = thing.a.do_it + .aligned + .more_alignment(); +} +" + ))) + +(ert-deftest indent-method-chains-no-let () + (let ((rust-indent-method-chain t)) (test-indent + " +fn main() { + thing.a.do_it + .aligned + .more_alignment(); +} +" + ))) + +(ert-deftest indent-method-chains-comment () + (let ((rust-indent-method-chain t)) (test-indent + " +fn main() { + // thing.do_it() + // .aligned() +} +" + ))) + +(ert-deftest indent-method-chains-close-block () + (let ((rust-indent-method-chain t)) (test-indent + " +fn main() { + foo.bar() +} +" + ))) diff --git a/rust-mode.el b/rust-mode.el index 70b49fea..b2e0d5dc 100644 --- a/rust-mode.el +++ b/rust-mode.el @@ -89,15 +89,6 @@ (backward-word 1)) (current-column)))) -(defun rust-align-to-method-chain () - (save-excursion - (previous-line) - (end-of-line) - (backward-word 1) - (backward-char) - (when (looking-at "\\..+\(.*\)\n") - (- (current-column) rust-indent-offset)))) - (defun rust-rewind-to-beginning-of-current-level-expr () (let ((current-level (rust-paren-level))) (back-to-indentation) @@ -105,6 +96,54 @@ (backward-up-list) (back-to-indentation)))) +(defun rust-align-to-method-chain () + (save-excursion + ;; for method-chain alignment to apply, we must be looking at + ;; another method call or field access or something like + ;; that. This avoids rather "eager" jumps in situations like: + ;; + ;; { + ;; something.foo() + ;; + ;; + ;; Without this check, we would wind up with the cursor under the + ;; `.`. In an older version, I had the inverse of the current + ;; check, where we checked for situations that should NOT indent, + ;; vs checking for the one situation where we SHOULD. It should be + ;; clear that this is more robust, but also I find it mildly less + ;; annoying to have to press tab again to align to a method chain + ;; than to have an over-eager indent in all other cases which must + ;; be undone via tab. + + (when (looking-at (concat "\s*\." rust-re-ident)) + (previous-line) + (end-of-line) + + (let + ;; skip-dot-identifier is used to position the point at the + ;; `.` when looking at something like + ;; + ;; foo.bar + ;; ^ ^ + ;; | | + ;; | position of point + ;; returned offset + ;; + ((skip-dot-identifier + (lambda () + (when (looking-back (concat "\." rust-re-ident)) + (backward-word 1) + (backward-char) + (- (current-column) rust-indent-offset))))) + (cond + ;; foo.bar(...) + ((looking-back ")") + (backward-list 1) + (funcall skip-dot-identifier)) + + ;; foo.bar + (t (funcall skip-dot-identifier))))))) + (defun rust-mode-indent-line () (interactive) (let ((indent @@ -123,10 +162,10 @@ (or (when rust-indent-method-chain (rust-align-to-method-chain)) - (save-excursion - (backward-up-list) - (rust-rewind-to-beginning-of-current-level-expr) - (+ (current-column) rust-indent-offset)))))) + (save-excursion + (backward-up-list) + (rust-rewind-to-beginning-of-current-level-expr) + (+ (current-column) rust-indent-offset)))))) (cond ;; A function return type is indented to the corresponding function arguments ((looking-at "->") @@ -137,16 +176,6 @@ ;; A closing brace is 1 level unindended ((looking-at "}") (- baseline rust-indent-offset)) - - ;;Line up method chains by their .'s - ((when (and rust-indent-method-chain - (looking-at "\..+\(.*\);?\n")) - (or - (let ((method-indent (rust-align-to-method-chain))) - (when method-indent - (+ method-indent rust-indent-offset))) - (+ baseline rust-indent-offset)))) - ;; Doc comments in /** style with leading * indent to line up the *s ((and (nth 4 (syntax-ppss)) (looking-at "*")) @@ -452,11 +481,84 @@ 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 + (< (- 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 ">")) + (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)) - ;;;###autoload (define-derived-mode rust-mode rust-parent-mode "Rust" "Major mode for Rust code." @@ -490,6 +592,7 @@ This is written mainly to be used as `end-of-defun-function' for Rust." (setq-local end-of-defun-function 'rust-end-of-defun) (setq-local parse-sexp-lookup-properties t) (add-hook 'syntax-propertize-extend-region-functions 'rust-syntax-propertize-extend-region) + (add-hook 'post-self-insert-hook 'rust-match-angle-bracket-hook) (setq-local syntax-propertize-function 'rust-syntax-propertize)) (defun rust-syntax-propertize-extend-region (start end)