Skip to content

Commit

Permalink
rewrite dostring folding
Browse files Browse the repository at this point in the history
  • Loading branch information
ddimitrov committed Jun 7, 2020
1 parent 03a7517 commit 22ef412
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 123 deletions.
211 changes: 88 additions & 123 deletions elpy.el
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
91 changes: 91 additions & 0 deletions test/elpy-folding-fold-all-docstrings-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -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))))

0 comments on commit 22ef412

Please sign in to comment.