customizing HideShow mode on Emacs is done by hs-special-modes-alist. There are 4 parts (actually 5, but we care mostly about the 3) of the equation. You can customize it for your mode with a list like this
(MODE START END COMMENT-START FORWARD-SEXP-FUNC ADJUST-BEG-FUNC)
For example, the default definition of the hs-special-modes-alist
looks like this:
(defvar hs-special-modes-alist
(mapcar #'purecopy
'((c-mode "{" "}" "/[*/]" nil nil)
(c-ts-mode "{" "}" "/[*/]" nil nil)
(c++-mode "{" "}" "/[*/]" nil nil)
(c++-ts-mode "{" "}" "/[*/]" nil nil)
(bibtex-mode ("@\\S(*\\(\\s(\\)" 1))
(java-mode "{" "}" "/[*/]" nil nil)
(java-ts-mode "{" "}" "/[*/]" nil nil)
(js-mode "{" "}" "/[*/]" nil)
(js-ts-mode "{" "}" "/[*/]" nil)
(mhtml-mode "{\\|<[^/>]*?" "}\\|</[^/>]*[^/]>" "<!--" mhtml-forward nil)
;; Add more support here.
))
The ingredients
START
and END
regular expression
This regular expression matches the beginning/end of the folding code. For C/C++ alike languages, this is as brackets "{", "}", etc. For some language lacks of any brackets/parenthesis like python, it is more difficult. Emacs' built-in python mode provided patch to support it:
(unless (assoc 'pyhon-mode hs-special-modes-alist)
(setq hs-special-modes-alist
(cons (list
'python-mode "^\\s-*def\\>" nil "#"
(lambda (arg)
(py-end-of-def-or-class)
(skip-chars-backward " \t\n"))
nil) hs-special-modes-alist)))
You can see that in for python mode, they did not define a END
expression, it is simply nil. This is due to how the hs-hide-block
works. We will see it below.
Comment regular expression is similar, it is used to match the comments.
FORWARD-SEXP-FUNC
The FORWARD-SEXP-FUNC
here is for jumping from the begging of the s-expression to its end. For the elisp that is basically the jumping between parenthesis. In C/C++, Java, etc. We can jump over brackets with forward-sexp
and backward-sexp
.
How it actually works
We take a look at the (hs-hide-blocks)
functions. It is written like this:
(defun hs-hide-block (&optional end)
"Select a block and hide it. With prefix arg, reposition at END.
Upon completion, point is repositioned and the normal hook
`hs-hide-hook' is run. See documentation for `run-hooks'."
(interactive "P")
(hs-life-goes-on
(let ((c-reg (hs-inside-comment-p)))
(cond
((and c-reg (or (null (nth 0 c-reg))
(<= (count-lines (car c-reg) (nth 1 c-reg)) 1)))
(message "(not enough comment lines to hide)"))
((or c-reg
(funcall hs-looking-at-block-start-p-func)
(funcall hs-find-block-beginning-func))
(hs-hide-block-at-point end c-reg)
(run-hooks 'hs-hide-hook))))))
This function takes condition with (hs-inside-comment-p)
, which returns nil
or a list of (BEGIN END)
of the comment. Then it tries to see if we only one line of comment, you get (not enough comment lines to hide)
in Message in this case. If we are not in the comments, the function tries to find the beginning using (hs-find-block-beginning)
of the block and call (hs-hide-block-at-point)
(hs-hide-block-at-point end comment-reg)
This function is the meat of the hide-show package. It does the actual hiding. It takes additional arguments of end
(which will go to block end if true) and comment-reg
as the comment region.
The function starts by:
(when (funcall hs-looking-at-block-start-p-func)
;;when we are at the begining of the block
(let ((mdata (match-data t))
(header-end (match-end 0))
p q ov)
;; `p' is the point at the end of the block beginning, which
;; may need to be adjusted. Adjustint p
(save-excursion
(goto-char (funcall (or hs-adjust-block-beginning #'identity)
header-end))
(setq p (line-end-position)))
;; We getting `q'
(hs-forward-sexp mdata 1) ;;going forward-by 1 s-expression
(setq q (if (looking-back hs-block-end-regexp nil)
(match-beginning 0)
(point)))
))
The (match-data t)
gives … and (match-end 0)
gives the position of end-of-text from last entire match. Afterwards we optionally adjust the block-beginning with defined hs-adjust-block-beginning
function. Finally, the q
is set to the (hs-forward-sexp mdata 1)
, which means we are going forward by 1 s-expression. The expression below:
(setq q (if (looking-back hs-block-end-regexp nil)
(match-beginning 0)
(point)))
Basically will set q to either a match-beggining
of matched hs-block-end-regexp
or simply (point)
Now we have everything, the requirement to make hs-hide-block-at-point
work is that
- We are at the match beginning of
hs-block-start-regexp
. - We can go forward to
hs-block-end-regexp
by jumping 1 s-expression. NOTE thehs-block-end-regexp
is not a hard requirement, because we can always setq
using(point)
. Remember in the python's setup before. Thehs-block-end-regexp
is actually simplynil
.
How we make it work for CMake Mode.
CMake language is not friendly to hide-show mode at all, just like Python. since it does not rely on parenthesis or brackets for organizing the code blocks, we have to come up with something creative, in the python case above, it is done by implementing a special hs-forward-sexp
using py-end-of-def-or-class
. This effectively makes the hide-show only works with method or class level, no folding for if
, for
, etc. (Though I think by checking the indentation level you can probably implementing it). Anyway, for CMake, I crafted the hs-block-start-regexp
and hs-block-end-regexp
.
hs-block-start-regexp
(setq hs-block-start-regexp
"^\\([:blank:]*\\)\\(?1:if\\|function\\|macro\\) *(.*)")
Without turning this block post into a Emacs regular expression crash course as well, the meaning of the expression here simply means to match line that maybe starts with blank spaces, followed by either if(someting)
, function(something)
, macro(something)
with maybe space before the parenthesis. This is also just one level folding, we pretty much ignore anything like elif ()
, else ()
.
hs-block-end-regexp
(setq hs-block-end-regexp "^\\([:blank:]*\\)end\\(?1:[A-Za-z]+\\) *(.*)")
The end part is actually really simple, as the language offers explicit syntax for this.
How we can test our matches using re-search-forward
;; should return if or function or macro
(prog2 (re-search-forward
"^\\([:blank:]*\\)\\(?1:if\\|function\\|macro\\) *(.*)")
(match-string-no-properties 1))
;; should return anything after end
(prog2 (re-search-forward "^\\([:blank:]*\\)end\\(?1:[A-Za-z]+\\) *(.*)")
(match-string-no-properties 1))
Implementing hs-forward-sexp
for CMake
Right now if we stands at the beginning of CMake if statement, what we can do, the obvious thing is we can do is search for a re-block-end-regexp
, however it may or may not be the same re-block-beginning-regexp
we are looking for. A simple example if we stands at function()
and next end expression would match a endif()
. What we want is to match the first endfunction())
we meet.
Here is an example of such implementation:
(defun hs-cmake-forward-sexp-once ()
"forward cmake s-expression once"
(when (looking-at hs-block-start-regexp)
(let ((matched-key-beg (match-string-no-properties 1))
(matched-key-end "")
end-point)
;; now we search for the end regular expression
(save-excursion
;; loop until we find the END that matches the START
(while (not (string= matched-key-beg matched-key-end))
;; search forward
(re-search-forward hs-block-end-regexp)
;; and updating the point and end match data.
(setq matched-key-end (match-string-no-properties 1))
(setq end-point (match-beginning 0)))
)
(goto-char end-point))))
The function (hs-cmake-forward-sexp-once)
will try to jump one cmake expression block. The (hs-cmake-forward-sexp ())
will do this N times.
(defun hs-cmake-forward-sexp (N)
"forward cmake s-expression ARGS times"
(dotimes (i N)
(hs-cmake-forward-sexp-once)))
Putting it all together
We simply insert our CMake settings to the hs-special-modes-alist
.
;; same as before
(defun hs-cmake-forward-sexp-once ()
...)
;; same as before
(defun hs-cmake-forward-sexp (args)
...)
(add-to-list 'hs-special-modes-alist
'(cmake-mode
;;match the begining
"^\\([:blank:]*\\)\\(?1:if\\|function\\|macro\\) *(.*)"
;match the end
"^\\([:blank:]*\\)end\\(?1:[A-Za-z]+\\) *(.*)"
;;comment
"#"
hs-cmake-forward-sexp
nil))