diff --git a/elpy.el b/elpy.el index b373ad2cd..c2904ddf5 100644 --- a/elpy.el +++ b/elpy.el @@ -3254,13 +3254,6 @@ display the current class and method instead." after-change-functions)) (elpy-folding--mark-foldable-lines))))) -(defvar elpy-folding-docstring-regex "[uU]?[rR]?\"\"\"" - "Regular expression matching docstrings openings and closings.") - -(defvar elpy-docstring-block-start-regexp - "^\\s-*[uU]?[rR]?\"\"\"\n?\\s-*" - "Version of `hs-block-start-regexp' for docstrings.") - ;; Indicators (defun elpy-folding--display-code-line-counts (ov) "Display a folded region indicator with the number of folded lines. @@ -3363,114 +3356,91 @@ Meant to be used as a hook to `after-change-functions'." (deactivate-mark))))))) ;; Hidding docstrings -(defun elpy-folding--hide-docstring-region (beg end) - "Hide a region from BEG to END, marking it as a docstring. - -BEG and END have to be respectively on the first and last line -of the docstring, their values are adapted to hide only the -docstring body." - (hs-life-goes-on - ;; do not fold oneliners - (when (not (save-excursion - (goto-char beg) - (beginning-of-line) - (re-search-forward - (concat elpy-folding-docstring-regex - ".*" - elpy-folding-docstring-regex) - (line-end-position) t))) - ;; get begining position (do not fold first doc line) - (save-excursion - (goto-char beg) - (when (save-excursion - (beginning-of-line) - (re-search-forward - (concat elpy-folding-docstring-regex - "[[:space:]]*$") - (line-end-position) t)) - (forward-line 1)) - (beginning-of-line) - (back-to-indentation) - (setq beg (point)) - (setq ov-beg (line-end-position))) - ;; get end position - (save-excursion - (goto-char end) - (setq end (line-beginning-position)) - (setq ov-end (line-end-position))) - (hs-discard-overlays ov-beg ov-end) - (hs-make-overlay ov-beg ov-end 'docstring (- beg ov-beg) (- end ov-end)) - (run-hooks 'hs-hide-hook) - (goto-char beg)))) - -(defun elpy-folding--hide-docstring-at-point () - "Hide the docstring at point." - (hs-life-goes-on - (let ((hs-block-start-regexp elpy-docstring-block-start-regexp)) - (when (and (python-info-docstring-p) (not (hs-already-hidden-p))) - (let (beg end line-beg line-end) - ;; Get first doc line - (if (not (save-excursion (forward-line -1) - (python-info-docstring-p))) - (setq beg (line-beginning-position)) - (forward-line -1) - (end-of-line) - (re-search-backward (concat "^[[:space:]]*" - elpy-folding-docstring-regex) - nil t) - (setq beg (line-beginning-position))) - ;; Go to docstring opening (to be sure to be inside the docstring) - (re-search-forward elpy-folding-docstring-regex nil t) - (setq line-beg (line-number-at-pos)) - ;; Get last line - (if (not (save-excursion (forward-line 1) - (python-info-docstring-p))) - (progn - (setq end (line-end-position)) - (setq line-end (line-number-at-pos))) - (re-search-forward elpy-folding-docstring-regex nil t) - (setq end (line-end-position)) - (setq line-end (line-number-at-pos))) - ;; hide the docstring - (when (not (= line-end line-beg)) - (elpy-folding--hide-docstring-region beg end))))))) - -(defun elpy-folding--show-docstring-at-point () - "Show docstring at point." - (hs-life-goes-on - (let ((hs-block-start-regexp elpy-docstring-block-start-regexp)) - (when (python-info-docstring-p) - (hs-show-block))))) - (defvar-local elpy-folding-docstrings-hidden nil "If docstrings are globally hidden or not.") +(defvar elpy-folding-class-def-start-regexp + "^\s*\\(\\bdef\\b\\|\\bclass\\b\\)") + +(defun elpy-folding-find-docstring-overlay-start () + "Find where should the docstring overlay start. + +Assume that point is at the first docstring delimiter." + (let (overlay-start) + (save-excursion + ;; Focusing on ''' or """ docstrings is enough in our case + (goto-char (+ 3 (point))) + (when (looking-at-p "[[:space:]]*$") + (forward-line)) + (setq overlay-start (line-end-position))) + overlay-start)) + +(defun elpy-folding-request-toggle-docstring-hiding-p () + "Decide whether to request docstring folding. + +Assume that point is at the first docstring delimiter." + (when (python-info-docstring-p) + (goto-char (elpy-folding-find-docstring-overlay-start)) + (let ((ov (hs-overlay-at (point)))) + (when (not (eq (not elpy-folding-docstrings-hidden) + (overlayp ov))) + ;; respect overlay if not of kind 'docstring + (unless (and ov + (not (eq (overlay-get ov 'hs) + 'docstring))) + t))))) + +(defun elpy-folding-search-docstring-delimiter () + "Search docstring delimiter while jumping over delimiters in comments." + (let ((in-comment t)) + (while in-comment + (re-search-forward "\"\\|\'" nil t) + (setq in-comment (nth 4 (syntax-ppss)))))) + (defun elpy-folding-toggle-docstrings () - "Fold or unfold every docstrings in the current buffer." + "Toggle hiding of all docstrings in the current buffer. + +A potential module-level docstring is handled first. A missing docstring is not +a problem." (interactive) - (if (not hs-minor-mode) - (message "Please enable the 'Folding module' to use this functionality.") - (hs-life-goes-on - (save-excursion - (goto-char (point-min)) - (while (python-nav-forward-defun) - (search-forward-regexp ")\\s-*:" nil t) - (forward-line) - (when (and (python-info-docstring-p) - (progn - (beginning-of-line) - (search-forward-regexp elpy-folding-docstring-regex - nil t))) - (forward-char 2) - (back-to-indentation) - ;; be sure not to act on invisible docstrings - (unless (and (hs-overlay-at (point)) - (not (eq (overlay-get (hs-overlay-at (point)) 'hs) - 'docstring))) - (if elpy-folding-docstrings-hidden - (elpy-folding--show-docstring-at-point) - (elpy-folding--hide-docstring-at-point))))))) - (setq elpy-folding-docstrings-hidden (not elpy-folding-docstrings-hidden)))) + (hs-life-goes-on + (save-excursion + (goto-char (point-min)) + (elpy-folding-search-docstring-delimiter) + (when (elpy-folding-request-toggle-docstring-hiding-p) + (elpy-folding-toggle-hide-docstring t)) + (while (re-search-forward elpy-folding-class-def-start-regexp nil t) + (search-forward-regexp ":" nil t) + (elpy-folding-search-docstring-delimiter) + (when (elpy-folding-request-toggle-docstring-hiding-p) + (elpy-folding-toggle-hide-docstring t))) + (setq elpy-folding-docstrings-hidden (not elpy-folding-docstrings-hidden))))) + +(defun elpy-folding-toggle-hide-docstring (&optional into-docstring) + "Toggle hiding of docstring at point. + +When INTO-DOCSTRING is t, we assume that we are in a docstring and don't bother +checking." + (when (or into-docstring (python-info-docstring-p)) + (let (docstring-end docstring-start overlay-start indent docstring-is-hidden) + (save-excursion + (python-nav-end-of-statement) + (setq docstring-end (point))) + (save-excursion + (python-nav-beginning-of-statement) + (setq docstring-start (point) + indent (current-column) + overlay-start (elpy-folding-find-docstring-overlay-start)) + (goto-char overlay-start) + (setq docstring-is-hidden (hs-overlay-at (point)))) + (if docstring-is-hidden + (save-excursion (hs-show-block)) + (when (> (count-lines docstring-start docstring-end) 1) + (goto-char overlay-start) + (move-to-column indent) + (hs-discard-overlays overlay-start docstring-end) + (hs-make-overlay overlay-start docstring-end 'docstring) + (run-hooks 'hs-hide-hook)))))) ;; Hiding comments (defvar-local elpy-folding-comments-hidden nil @@ -3567,19 +3537,14 @@ If a region is selected, fold that region." ;; Use selected region (if (use-region-p) (elpy-folding--hide-region (region-beginning) (region-end)) - ;; Adapt starting regexp if on a docstring - (let ((hs-block-start-regexp - (if (python-info-docstring-p) - elpy-docstring-block-start-regexp - hs-block-start-regexp))) - ;; Hide or fold - (cond - ((hs-already-hidden-p) - (hs-show-block)) - ((python-info-docstring-p) - (elpy-folding--hide-docstring-at-point)) - (t - (hs-hide-block)))))))) + ;; Hide or fold + (cond + ((hs-already-hidden-p) + (hs-show-block)) + ((python-info-docstring-p) + (elpy-folding-toggle-hide-docstring t)) + (t + (hs-hide-block))))))) ;;;;;;;;;;;;;;;;;;; ;;; Module: Flymake diff --git a/test/elpy-folding-fold-all-docstrings-test.el b/test/elpy-folding-fold-all-docstrings-test.el index af8c1dd21..fdddc1405 100644 --- a/test/elpy-folding-fold-all-docstrings-test.el +++ b/test/elpy-folding-fold-all-docstrings-test.el @@ -55,3 +55,94 @@ (should (= 4 (length overlays)))) ;; Position (should (= (point) 231)))) + +(ert-deftest elpy-fold-docstrings-handle-comments () + (elpy-testcase () + (add-to-list 'elpy-modules 'elpy-module-folding) + (set-buffer-string-with-point + "var1 = 45" + "" + "class foo(object):" + " def __init__(self, a, b): # FIXME: \"strange\" \'bug\'." + " # By \"strange\" I \'mean\' ..." + " \"\"\" " + " First docstring spawning " + " several li_|_nes." + " \"\"\"" + " self.a = a" + " self.b = b" + "" + "var2 = foo(var1, 4)") + (elpy-enable) + (python-mode) + (elpy-folding-toggle-docstrings) + (let* ((overlays (overlays-in (point-min) (point-max))) + overlay) + (setq overlay (elpy-get-overlay-at 156 'docstring)) + (should overlay) + (should (eq (overlay-get overlay 'hs) 'docstring)) + (should (= (overlay-start overlay) 156)) + (should (= (overlay-end overlay) 183))) + (should (= (point) 171)) + ;; Unfold + (elpy-folding-toggle-docstrings) + ;; Position + (should (= (point) 171)))) + +(ert-deftest elpy-fold-docstrings-handle-class-and-module-docstring () + (elpy-testcase () + (add-to-list 'elpy-modules 'elpy-module-folding) + (set-buffer-string-with-point + "\'\'\'This is a module-level docstring with different delimiters." + "" + "Apart from this, we should be able to handle docstrings of classes.\'\'\'" + "var1 = 45" + "" + "class foo(object):" + " \'\'\'This class might seem simple, but don't stop reading." + "" + " What did you expe_|_ct - there is no magic!" + " \'\'\'" + " def __init__(self, a, b): # FIXME: \"strange\" \'bug\'." + " # By \"strange\" I \'mean\' ..." + " \"\"\" " + " First docstring spawning " + " several lines." + " \"\"\"" + " self.a = a" + " self.b = b" + "" + "note = \"\"\"Clearly, this is not a docstring" + "and folding it would be bad." + "But we are on the safe side (well, maybe).\"\"\"" + "" + "var2 = foo(var1, 4)") + (elpy-enable) + (python-mode) + (elpy-folding-toggle-docstrings) + (let* ((overlays (overlays-in (point-min) (point-max))) + overlay) + ;; Module-level docstring + (setq overlay (elpy-get-overlay-at 63 'docstring)) + (should overlay) + (should (eq (overlay-get overlay 'hs) 'docstring)) + (should (= (overlay-start overlay) 63)) + (should (= (overlay-end overlay) 135)) + ;; Class docstring + (setq overlay (elpy-get-overlay-at 224 'docstring)) + (should overlay) + (should (eq (overlay-get overlay 'hs) 'docstring)) + (should (= (overlay-start overlay) 224)) + (should (= (overlay-end overlay) 274)) + ;; Method docstring + (setq overlay (elpy-get-overlay-at 400 'docstring)) + (should overlay) + (should (eq (overlay-get overlay 'hs) 'docstring)) + (should (= (overlay-start overlay) 400)) + (should (= (overlay-end overlay) 427)) + ) + (should (= (point) 245)) + ;; Unfold + (elpy-folding-toggle-docstrings) + ;; Position + (should (= (point) 245))))