diff --git a/rust-mode-tests.el b/rust-mode-tests.el index 3252b6a4..197710e0 100644 --- a/rust-mode-tests.el +++ b/rust-mode-tests.el @@ -891,6 +891,60 @@ struct Foo { } " rust-test-region-string rust-test-motion-string + rust-test-doctest-motion-string +" +//! This is crate-level documentation. +//! This is some more crate-level documentation. +//! This is a third line. +//! +//! This is a fourth line. +//! +//! ```rust +//! prepare_something(); +//! do_something(); +//! ``` + +/// This is a doc comment. +/// This is more. +fn is_documented() { + true; +} + +// This is not a doc comment. +fn undocumented() { + false; +} + +struct Foo {} + +impl Foo { + /// Do a foo. + /// + /// # Examples + /// + /// ```rust + /// assert!(foo()); + /// ``` + fn foo() { + true; + } +} + +/// Do a bar (dangerous). +/// +/// # Examples +/// +/// ```rust,no_run +/// if (do_unsafe_things) { +/// bar(); +/// } else { +/// foo(); +/// } +/// ``` +fn bar() { + // dangerous! + unsafe { } +}" rust-test-indent-motion-string " fn blank_line(arg:i32) -> bool { @@ -948,7 +1002,17 @@ fn indented_already() { (nonblank-line-indented-already-bol-start (21 0)) (nonblank-line-indented-already-bol-target (21 4)) (nonblank-line-indented-already-middle-start (21 2)) - (nonblank-line-indented-already-middle-target (21 4)))) + (nonblank-line-indented-already-middle-target (21 4)) + (before-doctest1 (7 0)) + (declaration-of-doctest1 (8 0)) + (inside-doctest1 (9 10)) + (inside-doctest1-slash (9 1)) + (after-doctest1 (11 7)) + (before-doctest2 (30 0)) + (declaration-of-doctest2 (31 0)) + (inside-doctest2 (32 10)) + (inside-doctest2-slash (32 6)) + (after-doctest2 (33 11)))) (defun rust-get-buffer-pos (pos-symbol) "Get buffer position from POS-SYMBOL. @@ -1123,6 +1187,78 @@ fn test4();") 'between-fn1-fn2 #'end-of-defun -2)) +(ert-deftest rust-beginning-of-doctest-in-doctest () + (rust-test-motion + rust-test-doctest-motion-string + 'inside-doctest1 + 'declaration-of-doctest1 + #'rust-beginning-of-doctest) + (rust-test-motion + rust-test-doctest-motion-string + 'inside-doctest2 + 'declaration-of-doctest2 + #'rust-beginning-of-doctest)) + +(ert-deftest rust-beginning-of-doctest-in-doctest-slash () + (rust-test-motion + rust-test-doctest-motion-string + 'inside-doctest1-slash + 'declaration-of-doctest1 + #'rust-beginning-of-doctest) + (rust-test-motion + rust-test-doctest-motion-string + 'inside-doctest2-slash + 'declaration-of-doctest2 + #'rust-beginning-of-doctest)) + +(ert-deftest rust-beginning-of-doctest-after-doctest () + (rust-test-motion + rust-test-doctest-motion-string + 'after-doctest1 + 'declaration-of-doctest1 + #'rust-beginning-of-doctest) + (rust-test-motion + rust-test-doctest-motion-string + 'after-doctest2 + 'declaration-of-doctest2 + #'rust-beginning-of-doctest)) + +(ert-deftest rust-end-of-doctest-in-doctest () + (rust-test-motion + rust-test-doctest-motion-string + 'inside-doctest1 + 'after-doctest1 + #'rust-end-of-doctest) + (rust-test-motion + rust-test-doctest-motion-string + 'inside-doctest2 + 'after-doctest2 + #'rust-end-of-doctest)) + +(ert-deftest rust-end-of-doctest-in-doctest-slash () + (rust-test-motion + rust-test-doctest-motion-string + 'inside-doctest1-slash + 'after-doctest1 + #'rust-end-of-doctest) + (rust-test-motion + rust-test-doctest-motion-string + 'inside-doctest2-slash + 'after-doctest2 + #'rust-end-of-doctest)) + +(ert-deftest rust-end-of-doctest-after-doctest () + (rust-test-motion + rust-test-doctest-motion-string + 'before-doctest1 + 'after-doctest1 + #'rust-end-of-doctest) + (rust-test-motion + rust-test-doctest-motion-string + 'before-doctest2 + 'after-doctest2 + #'rust-end-of-doctest)) + (ert-deftest rust-mark-defun-from-middle-of-fn () (rust-test-region rust-test-region-string @@ -3179,10 +3315,202 @@ impl Two<'a> { ("file3.rs" "12" "34" compilation-warning "file3.rs:12:34")) matches))))) +(ert-deftest rust-recognizes-doc-comment () + (dolist (test '(("///" 0 1) + ("\n///" 2 2) + ("/// ```rust +/// ```" 13 13) + ("/// ```rust +/// ```" 20 13) + ("/// ```rust +/// ```" 1 1) + ("/// ```rust +/// ```" 0 1) + ("/// ```rust +/// ```" 8 1) + ("/// ```rust +/// ```" 15 13))) + (with-temp-buffer + (destructuring-bind (str cursor expected) test + (insert str) + (goto-char cursor) + (should (eq (rust-in-doc-comment-p) t)) + (should (= (point) expected))))) + (dolist (test '(("//!" 0 1) + ("\n//!" 2 2) + ("//! ```rust +//! ```" 13 13) + ("//! ```rust +//! ```" 20 13) + ("//! ```rust +//! ```" 1 1) + ("//! ```rust +//! ```" 0 1) + ("//! ```rust +//! ```" 8 1) + ("//! ```rust +//! ```" 15 13))) + (with-temp-buffer + (destructuring-bind (str cursor expected) test + (insert str) + (goto-char cursor) + (should (eq (rust-in-doc-comment-p) t)) + (should (= (point) expected)))))) + +(ert-deftest rust-uncomments-doctest () + (dolist (pair '(("" . "") ; no-op + ("// a" . "// a") ; double-slashed, don't touch + ("///" . "") + ("/// " . "") + ("/// " . "") + ("///foo\(\);" . "foo\(\);") + ("/// foo\(\);" . "foo\(\);") + ("/// foo\(\);" . "foo\(\);") + (" ///foo\(\);" . "foo\(\);") + (" /// foo\(\);" . "foo\(\);") + (" /// foo\(\);" . "foo\(\);") + ("// a\n///foo\(\);" . "// a\nfoo\(\);") + ("// a\n ///foo\(\);" . "// a\nfoo\(\);") + ("/// foo\(\);\n/// bar\(\);" . "foo\(\);\nbar\(\);") + ("/// foo\(\);\n/// bar\(\);" . "foo\(\);\n bar\(\);"))) + (with-temp-buffer + (insert (car pair)) + (rust-uncomment-doctest (point-min) (point-max)) + (should (string= (buffer-string) (cdr pair))))) + (dolist (pair '(("//!" . "") + ("//! " . "") + ("//! " . "") + ("//!foo\(\);" . "foo\(\);") + ("//! foo\(\);" . "foo\(\);") + ("//! foo\(\);" . "foo\(\);") + (" //!foo\(\);" . "foo\(\);") + (" //! foo\(\);" . "foo\(\);") + (" //! foo\(\);" . "foo\(\);") + ("// a\n//!foo\(\);" . "// a\nfoo\(\);") + ("// a\n //!foo\(\);" . "// a\nfoo\(\);") + ("//! foo\(\);\n//! bar\(\);" . "foo\(\);\nbar\(\);") + ("//! foo\(\);\n//! bar\(\);" . "foo\(\);\n bar\(\);"))) + (with-temp-buffer + (insert (car pair)) + (rust-uncomment-doctest (point-min) (point-max)) + (should (string= (buffer-string) (cdr pair)))))) + +(ert-deftest rust-comments-doctest () + (dolist (pair '(("" . "/// ") + ("// a" . "/// // a") + ("///foo\(\);" . "/// ///foo\(\);") + ("/// foo\(\);" . "/// /// foo\(\);") + ("/// foo\(\);" . "/// /// foo\(\);") + (" ///foo\(\);" . "/// ///foo\(\);") + (" /// foo\(\);" . "/// /// foo\(\);") + (" /// foo\(\);" . "/// /// foo\(\);") + ("// a\n///foo\(\);" . "/// // a\n/// ///foo\(\);") + ("// a\n ///foo\(\);" . "/// // a\n/// ///foo\(\);"))) + (with-temp-buffer + (insert (car pair)) + (rust-comment-doctest (point-min) (point-max)) + (should (string= (buffer-string) (cdr pair))))) + (dolist (pair '(("" . "//! ") + ("// a" . "//! // a") + ("//!foo\(\);" . "//! //!foo\(\);") + ("//! foo\(\);" . "//! //! foo\(\);") + ("//! foo\(\);" . "//! //! foo\(\);") + (" //!foo\(\);" . "//! //!foo\(\);") + (" //! foo\(\);" . "//! //! foo\(\);") + (" //! foo\(\);" . "//! //! foo\(\);") + ("// a\n//!foo\(\);" . "//! // a\n//! //!foo\(\);") + ("// a\n //!foo\(\);" . "//! // a\n//! //!foo\(\);"))) + (with-temp-buffer + (insert (car pair)) + (rust-comment-doctest (point-min) (point-max) "//!") + (should (string= (buffer-string) (cdr pair)))))) + +(ert-deftest rust-beginning-of-doc-comment-block-works () + (dolist (test '(("///" 1 1) + ("///" 2 1) + ("///" 3 1) + (" ///" 1 1) + (" ///" 5 1) + ("///\n///" 1 1) + ("///\n///" 4 1) + ("///\n///" 5 1) + (" ///\n ///" 1 1) + (" ///\n ///" 4 1) + (" ///\n ///" 5 1) + (" ///\n ///" 11 1) + ("\n///" 2 2) + ("\n///" 4 2) + ("\n///" 5 2) + ("\n///\n" 2 2) + ("\n///\n" 4 2) + ("\n///\n" 5 2))) + (with-temp-buffer + (destructuring-bind (text cursor expected) test + (insert text) + (goto-char cursor) + (rust-beginning-of-doc-comment-block) + (should (= (point) expected))))) + (dolist (test '(("//!" 1 1) + ("//!" 2 1) + ("//!" 3 1) + (" //!" 1 1) + (" //!" 5 1) + ("//!\n//!" 1 1) + ("//!\n//!" 4 1) + ("//!\n//!" 5 1) + (" //!\n //!" 1 1) + (" //!\n //!" 4 1) + (" //!\n //!" 5 1) + (" //!\n //!" 11 1) + ("\n//!" 2 2) + ("\n//!" 4 2) + ("\n//!" 5 2) + ("\n//!\n" 2 2) + ("\n//!\n" 4 2) + ("\n//!\n" 5 2))) + (with-temp-buffer + (destructuring-bind (text cursor expected) test + (insert text) + (goto-char cursor) + (rust-beginning-of-doc-comment-block) + (should (= (point) expected)))))) + ;; 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) +(when (and (require 'noflet nil t) + (require 'edit-indirect nil t)) ; we won't use it in the + ; test but the function + ; won't be fully defined + ; without it + (lexical-let ((edit-doctest-string1 "/// ```rust\n/// foo\(\);\n/// ```") + (edit-doctest-string2 "/// ```rust\n/// ```") + (edit-doctest-string2-output "/// ```rust\n/// \n/// ```") + (edit-doctest-string3 "fn no_doc() {\n println!(\"No documentation here\"); // nothing else to say\n}\n\n/// Prints `hello`.\n/// \n/// ```rust\n/// print_hello();\n/// ```\nfn print_hello() {\n print!(\"hello\");\n}\n\n/// Doesn't print `hello`.\n/// \n/// ```rust\n/// if true {\n/// dont_print_hello();\n/// }\n/// ```\nfn dont_print_hello() {\n print!(\"bye\");\n}") + (edit-doctest-string4 "fn no_doc() {\n println!(\"No documentation here\"); // nothing else to say\n}\n\n//! Prints `hello`.\n//! \n//! ```rust\n//! print_hello();\n//! ```\nfn print_hello() {\n print!(\"hello\");\n}\n\n//! Doesn't print `hello`.\n//! \n//! ```rust\n//! if true {\n//! dont_print_hello();\n//! }\n//! ```\nfn dont_print_hello() {\n print!(\"bye\");\n}")) + (ert-deftest rust-edit-doctest-works () + (dolist (test `((,edit-doctest-string1 12 ,edit-doctest-string1 13 23) + (,edit-doctest-string1 13 ,edit-doctest-string1 13 23) + (,edit-doctest-string1 24 ,edit-doctest-string1 13 23) + (,edit-doctest-string1 31 ,edit-doctest-string1 13 23) + (,edit-doctest-string2 1 ,edit-doctest-string2-output 13 17) + (,edit-doctest-string2 12 ,edit-doctest-string2-output 13 17) + (,edit-doctest-string2 13 ,edit-doctest-string2-output 13 17) + (,edit-doctest-string3 123 ,edit-doctest-string3 117 135) + (,edit-doctest-string3 257 ,edit-doctest-string3 231 278) + (,edit-doctest-string4 123 ,edit-doctest-string4 117 135) + (,edit-doctest-string4 257 ,edit-doctest-string4 231 278))) + (destructuring-bind (initial-text cursor final-text expected-beg expected-end) test + (noflet ((edit-indirect-region (beg end &optional display-buffer) + (should (= beg expected-beg)) + (should (= end expected-end)))) + (with-temp-buffer + (insert initial-text) + (goto-char cursor) + (rust-edit-doctest) + (should (equal (buffer-string) final-text))))))))) + ;; The emacs 23 and 24 versions of ERT do not have test skipping ;; functionality. So don't even define these tests if elec-pair is ;; not available. diff --git a/rust-mode.el b/rust-mode.el index b361749e..ae7bd945 100644 --- a/rust-mode.el +++ b/rust-mode.el @@ -1617,7 +1617,10 @@ Return the created process." (setq-local rust-buffer-project nil) (when rust-always-locate-project-on-open - (rust-update-buffer-project))) + (rust-update-buffer-project)) + + (when (featurep 'edit-indirect) + (rust--setup-doctest-hooks))) ;;;###autoload (add-to-list 'auto-mode-alist '("\\.rs\\'" . rust-mode)) @@ -1776,6 +1779,182 @@ visit the new file." (let ((output (json-read))) (cdr (assoc-string "root" output)))))) +(defvar rust-doc-comment-re "\\(^[[:blank:]]*//[!/]\\)\\([[:blank:]]*\\)" + "Regular expression to match Rust doc comments.") + +(defvar rust-doctest-re "^[[:blank:]]*//[!/][[:blank:]]*```rust\\(,.*\\)?$" + "Regular expression to match Rust doctests.") + +(defvar rust-end-of-doctest-re "^[[:blank:]]*//[!/][[:blank:]]*```$" + "Regular expression to match the ends of Rust doctests. + +This is not enough on its own to find them as the closing quotes +are ambiguous: they may belong to doctests or to any other +Markdown code block.") + +(defun rust-in-doc-comment-p () + "Return T if the current line is a Rust doc comment. Moves +point to beginning of line." + (beginning-of-line) + (looking-at rust-doc-comment-re)) + +(defun rust-beginning-of-doctest-p () + "Return T if the current line is the beginning of a Rust doctest." + (save-excursion + (beginning-of-line) + (looking-at rust-doctest-re))) + +(defun rust-beginning-of-doc-comment-block () + "Go to the start of the Rust doc comment block around point. If +there is no such block, do nothing." + (let (pos + stop) + (while (and (null stop) (rust-in-doc-comment-p)) + (setq pos (point)) + (if (bobp) + (setq stop t) + (forward-char -1))) + (when pos + (goto-char pos)))) + +(defun rust-beginning-of-doctest () + "Go to the beginning of the Rust doctest around point. If there +is no such block, do nothing." + (interactive) + (when (rust-in-doc-comment-p) + (let ((bound + (save-excursion (rust-beginning-of-doc-comment-block)))) + (when (not (rust-beginning-of-doctest-p)) + (re-search-backward rust-doctest-re bound))))) + +(defun rust-end-of-doctest () + "Go to the end of the Rust doctest around point." + (interactive) + (beginning-of-line) + (re-search-forward rust-end-of-doctest-re)) + +(defvar rust-doctest-mode-syntax-table + (let ((table (make-syntax-table rust-mode-syntax-table))) + (modify-syntax-entry ?# "< b" table) + table) + "Syntax table for `rust-doctest-mode'. + +Recognizes comments marked with `#' but cannot recognize escaped +comments \(`##' and so on\).") + +;;;###autoload +(define-derived-mode rust-doctest-mode rust-mode "Rust-Doctest" + "Major mode for Rust doctests. + +\\\{rust-mode-map\}" + :group 'rust-mode + :syntax-table rust-doctest-mode-syntax-table + + (setq-local comment-start-skip "\\(?:\\(?:#\\)\\|//[/!]*\\|/\\*[*!]?\\)[[:space:]]*")) + +(defun rust-uncomment-doctest (beg end-position) + "Remove Rust doc comment markers and common whitespace from the +start of each line between BEG and END-POSITION. + +Any lines not starting with the markers will be untouched. + +Only the amount of whitespace common to every line will be +removed." + (interactive "r") + (save-excursion + (let ((end (make-marker))) + (set-marker end end-position) + (goto-char beg) + (let (minimum) + (while (re-search-forward rust-doc-comment-re end t) + (replace-match "" nil nil nil 1) + (let ((l (length (match-string 2)))) + (when (or (null minimum) (< l minimum)) + (setq minimum l)))) + (when (not (null minimum)) + (goto-char beg) + (catch 'done + (while (re-search-forward "^" end t) + (delete-char minimum) + (when (eobp) + (throw 'done nil)) + (forward-line 1))))))) + (goto-char beg)) + +(defun rust-comment-doctest (beg end-position &optional comment-marker) + "Add Rust doc comment markers to the start of each line between +BEG and END-POSITION. + +If COMMENT-MARKER is non-nil, it will be used as the marker. If +it is nil, the string `///' will be used. + +Interactively, will prompt for COMMENT-MARKER." + (interactive "r\nM") + (when (null comment-marker) + (setq comment-marker "///")) + (save-excursion + (let ((end (make-marker))) + (set-marker end end-position) + (set-marker-insertion-type end t) + (goto-char beg) + (while (re-search-forward "^" end t) + (insert (concat comment-marker " "))) + (set-marker end nil)))) + +(defun rust--uncomment-doctest-in-buffer () + "Run `rust-uncomment-doctest' on the text in the current buffer." + (rust-uncomment-doctest (point-min) (point-max))) + +(defun rust--comment-doctest-in-buffer () + "Run `rust-comment-doctest' on the text in the current buffer." + (rust-comment-doctest (point-min) (point-max))) + +(defun rust-edit-doctest () + "Edit the doctest around point in a separate indirect buffer. +Requires the `edit-indirect' package." + (interactive) + (error "This feature requires the `edit-indirect' package.")) + +(defun rust--setup-doctest-hooks () + nil) + +(require 'edit-indirect nil t) + +(eval-after-load 'edit-indirect + '(progn + ;; silence warnings + (declare-function edit-indirect-region "edit-indirect.el") + (defvar edit-indirect-after-commit-functions) + + (define-key rust-mode-map (kbd "C-c C-e") #'rust-edit-doctest) + + (defun rust-edit-doctest () + "Edit the doctest around point in a separate indirect +buffer. Requires the `edit-indirect' package." + (interactive) + (let ((beg (save-excursion + (rust-beginning-of-doctest) + (forward-line) + (line-beginning-position))) + (end (save-excursion + (rust-end-of-doctest) + (forward-line -1) + (line-end-position)))) + (when (<= end beg) + (rust-beginning-of-doctest) + (end-of-line) + (insert "\n/// ") + (setq end (point))) + (save-mark-and-excursion + (edit-indirect-region beg end t)))) + + (defun rust--setup-doctest-hooks () + "Add hooks for doctest buffers created from current Rust buffer." + (add-hook 'edit-indirect-after-creation-hook #'rust-doctest-mode nil t) + (add-hook 'edit-indirect-after-creation-hook #'rust--uncomment-doctest-in-buffer nil t) + (add-hook 'edit-indirect-before-commit-hook #'rust--comment-doctest-in-buffer nil t) + (add-to-list 'edit-indirect-after-commit-functions #'indent-region)))) + (provide 'rust-mode) ;;; rust-mode.el ends here