这可能是emacs里面最佳的折叠显示方案

前段时间我分享了我个人的配置 里面有这么一段描述

这里面讲到的原生折叠的问题 具体是什么呢? 直接看代码

if (1) {
// ...
} else {
// ...
}

如果直接用内置的hs折叠 效果是这样的

if和else被折叠成一行了 这个肯定是不能接受的 所以我重新写了一个overlay显示 代码如下 短短几行 让我看到了原子弹爆炸

(defun my-hs-set-up-overlay-c-style (ov)
  (goto-char (1+ (overlay-end ov)))
  (move-overlay ov (overlay-start ov) (1- (line-beginning-position)))
  (overlay-put ov 'display (propertize nn-fold-string 'face 'nn-fold-face)))

现在的效果是这样的

if和else不被折叠成一行 而是两行了 这点和常见的编辑器vscode是一模一样的 虽然vscode是基于缩进折叠的

代码原理如下 (goto-char (1+ (overlay-end ov))) 这个其实无关紧要 主要是我用savefold这个插件 每次打开buffer的时候 要恢复折叠状态 如果不定位位置就折叠的话 会恢复出错 我下面折叠的范围是(1- (line-beginning-position) 所以下一次打开文件要恢复的位置是(1+ (overlay-end ov)

接下来重点看这个 (move-overlay ov (overlay-start ov) (1- (line-beginning-position)))

这个就是定位到闭括号结束也就是}的位置 然后减1也就是 } 这个字符串的上一行 也就是改变折叠范围 从 {} 的上一行 代码非常巧妙 短短几行 让我看了好几天的hs源码才理解的… 直接瘫坐在椅子上了 遇到过行号被软折叠 折叠范围不对等等问题

最后配置在这里

9 个赞

省流

(use-package hideshow
  :ensure nil
  :hook
  (prog-mode . hs-minor-mode)
  :custom
  (hs-allow-nesting t)
  (hs-hide-comments-when-hiding-all nil)
  :config
  (defun my-hs-set-up-overlay-c-style (ov)
    (goto-char (1+ (overlay-end ov)))
    (move-overlay ov (overlay-start ov) (1- (line-beginning-position)))
    (overlay-put ov 'display (propertize nn-fold-string 'face 'nn-fold-face)))

  (defun my-hs-set-up-overlay (ov)
    (when (eq 'code (overlay-get ov 'hs))
      (if (derived-mode-p 'c-mode 'c-ts-mode 'c++-mode 'c++-ts-mode
                          'simpc-mode 'typescript-ts-mode 'tsx-ts-mode)
          (my-hs-set-up-overlay-c-style ov)
        (overlay-put ov 'display (propertize nn-fold-string 'face 'nn-fold-face)))))

  (setq hs-set-up-overlay #'my-hs-set-up-overlay
        hs-special-modes-alist
        '((c++-ts-mode "\\s(" "\\s)" "/[*/]" nil nil)
          (c-ts-mode "\\s(" "\\s)" "/[*/]" nil nil)
          (c++-mode "\\s(" "\\s)" "/[*/]" nil nil)
          (c-mode "\\s(" "\\s)" "/[*/]" nil nil)
          (simpc-mode "\\s(" "\\s)" "/[*/]" nil nil)
          (js-ts-mode "\\s(" "\\s)" "/[*/]" nil nil)
          (js-mode "\\s(" "\\s)" "/[*/]" nil nil)
          (typescript-ts-mode "\\s(" "\\s)" "/[*/]" nil nil)
          (nxml-mode "<!--\\|<[^/>]*[^/]>"
                     "-->\\|</[^/>]*[^/]>"
                     "<!--" sgml-skip-tag-forward nil)
          (t))))
4 个赞

这个感觉超级超级厉害,我写php时if(1){… }eliseif(){…}eliseif(){…}eliseif(){…}else{…} 这些都折叠成一行,以前居然也没有发现那里不对,看了楼主的帖子才发现它其实不应该是这个样子的。

另外AI(chatgpt)我也有询问(not english,date 20260620),直接了当的回复我hs-minor-mode做不到这一点,但楼主不仅做到了这一点,还把帖子发出来,等着AI去学习,这么看楼主比AI更强,AI不过是站在巨人的肩膀上而已,让它从肩膀上下来就比不了人类了

2 个赞

Hideshow的作者現在在reddit 發帖詳解hs的變化,你可以把這個折疊solution回复他

另外這裡的nn-fold-…在我的cfg裡沒有定義,是否可以去掉啊?

nn-fold-string 那个是我自定义的变量 具体的你可以看我仓库里面的配置 你可以换成你想显示的任何字符串 然后可以的话 你可以帮我把这个链接给他

先給你貼在這裡

I don’t tend to write things like this, but since this emacs carnival is about the “hidden gems” inside emacs, I couldn’t resist writing this (and because there are no posts talking about this)

Spoiler! This talks about Hideshow from emacs 31.1, so if you don’t want to get spoiled until 31.1 is released or until mickey writes his “What’s New…” post, don’t read this.

Code folding is something that maybe people here don’t use, but I’m the kind of person who still find it useful. Packages for this exists already in emacs, the well-know is outline (and its minor mode)… and the lesser-know is hideshow which 3-10+ years ago used to be problematic, however for this emacs-31 the package has received big changes, improving many things.

The package is almost as old as Emacs, it was written by TTN and Dan Nicolaescu, it was mostly intended for Lisp and C-like languages (or any language with parens and comments) , back then it was more than enough, however, when new tools and languages arrived, it was becoming more limited, for example, it didn’t supported python-mode simply because it was not designed for that kind of language.

Of course its support was added later, but it was still limited, and also more folding capabilities were still needed.

It is for this reason that origami.el and yafolding.el were created, the former being more customizable, and the latter only for indentation-based folding (nowadays, both are dead).

I really like hideshow but, i knew if it wanted to survive, it must be rewritten to be what it was not back then.

One thing I did was become its maintainer because: Who would maintain all the changes I made?

So here is a list of changes done to hideshow (6.0) for this emacs 31.1 (mostly made by me):

A complete rewrite:

hideshow was old, if I wanted to make it extendable, i had to get rid of the old code, prettify it and document it was well as possible.

I also had to partially get rid of the old logic of using regexp for the folding (which was limited) to allow using functions for more complex folding.

Also, this should make creating commands easier.

A quasi-API for extensibility

One of the things i had to get rid was hs-special-modes-alist, which was hard to configure and read, instead, it was replaced by many local variables (which were all the possible options of this variable), which makes it easier to configure it and apply it to its derived modes.

This brings a quasi-API (I’m not sure if this is actually an API), allowing hideshow to be extended more easily.

hs-cycle and hs-toggle-all

These was one of the commands requested for hideshow, more specifically hs-cycle for org-like cycling, there were packages to add this command, but however all of those are obsolete, i added this command based on Karthinks code (who gave me permission), and I improved it to support comments as well, this required changing hs-hide-level logic, so that command have slightly changed its behavior. r/emacs - Underappreciated Emacs built-ins: hideshow 6.0

I’ve also added hs-toggle-all (also based in Karthinks code), I don’t use this command often, but if people find it useful I added it, it doesn’t work like hs-cycle, for that use hs-hide-level with a number argument instead. r/emacs - Underappreciated Emacs built-ins: hideshow 6.0

Folding is now easier do to

To fold a block or comment one had to move the cursor to the block beginning, now this can be done regardless of where the cursor is positioned in the headline.

Also, hideshow commands no longer move the cursor to the end of the line.

Indentation-based folding

Perhaps one of the most requested, hideshow already supported indentation folding but only to what is defined in python-mode, I’ve added this feature (works similar to yafolding, don’t expect to outline-indent) to allow use it elsewhere plus a better delimiter closing: r/emacs - Underappreciated Emacs built-ins: hideshow 6.0 r/emacs - Underappreciated Emacs built-ins: hideshow 6.0

This gives an example to what hideshow can do and allow, even probably adding LSP folding support.

Proper treesit.el support:

Hideshow was coming with tree-sitter support, but it was limited, basing on my experience porting ts-fold to treesit.el (what became treesit-fold.el), i added a few things to allow major modes to decide how to configure it.

It works out-the-box for the built-in tree-sitter modes (well, most of them), for third-party, the package maintainers must configure hideshow manually.

Visual Indicators

Not many folding packages add support for indicators (the only one maybe was ts/treesit-fold), this is useful to know what can actually be folded, for hideshow there is hideshowvis, I used it with hideshow, but it was buggy. The disappointment I had when I learned that the creator rejected the offer to move it to GNU ELPA (and thus to core), motivated me to implement this, unlike hideshowvis, the built-in supports 3 types of indicators: fringes, margins, and EOL; and work both for TUI and GUI.

Display how many lines are folded

Also inspired by hideshowvis and treesit-fold, this allows to see how many lines are hidden in a block, nothing more. r/emacs - Underappreciated Emacs built-ins: hideshow 6.0

Easily change the prefix keymap

It’s well-know hideshow have a hard prefix key “C-c @”, i’ve changed how this prefix is defined so, if you want to change it to another key prefix you can just do:

:bind (:map hs-minor-mode-map (“C-c @” . nil) (“M-h” . hs-prefix-map))

TAB cycling folding

This was not added by me, but by Juri Linkov, this is a port of outline-minor-mode-cycle, allows to use the TAB key to toggle the folding depending of the context (e.g. If you are at the BOL or EOL or everywhere on the headline, etc…).

Most heavy stuff for hideshow were done, and i intend to still making it more extensible, for example, support multiple folding systems at the same time.

So that’s, i invite people to use and experiment with the current hideshow.el version in this emacs-31

附议

我平时不逛reddit 我看看有机会去发表一下评论吧

我发了下,如果措辞有问题大家可以评论,我去改。

2 个赞

我用treesit-fold-mode, 下面是截图,支持高亮语法

treesit-fold也解决不了 我改的是overlay

Looks like new hs-indentation-mode solves this problem.

我有空会试试 但我这个是改的hs-minor-mode的overlay 基于缩进的和基于minor的还是不一样的 minor是更接近treesit那种

現在這兩可以同時打開

如果这2个是能同时打开的话 那就很好了 因为基于缩进折叠范围的语义很大 比如下面这段cpp代码

class Person {
private:
  int name = 0;

public:
  int getName() {
    return name;
  }
}

这是基于 hs-indentation-mode的

如果是基于lsp的语义来折叠的话 是不会这样的 但是hs-minor-mode会更接近lsp的语义折叠范围

这是基于hs-minor-mode的

不会识别 label: 这种 但是基于缩进折叠的也有好处 那就是它会识别宏的折叠 看下面这个代码

#ifdef NN
  #define A
#endif

hs-minor-mode明显是做不到折叠它的 但是 hs-indentation-mode 可以 因为它是基于缩进的 如果2个能一起使用的话 我会设置一个优化级 比如先尝试 hs-indentation-mode 如果不行就用hs-minor-mode 为了更接近的折叠语义 我会让hs-minor-mode先折叠 它折叠不了就hs-indentation-mode去折叠 因为hs-minor-mode的折叠往往比基于缩进的更接近lsp的语义折叠

不过我有一个问题了 这2个模式的overlay是共用同一个 还是分开的?我晚点会仔细研究一下

https://www.reddit.com/r/emacs/comments/1uafbsn/underappreciated_emacs_builtins_hideshow_60/

无论怎么说 如果2个能一起开的话 再我看来体验是质的提升

不过我开发这个功能的时候大概是1-2个星期以前 那个时候我还在使用emacs30 那个版本的hs还没有这个功能 因为这个契机让我写了这个功能 但是我最近在我的电脑上编译了主分支的emacs32版本 它包含这个功能 我这个功能对于版本号低于31的依然有用

3 个赞

我emacs32测试发现这2个如果一起打开的话 只会听hs-indentation-mode的 不会听hs-minor-mode的

我看Reddit的回复试了下这个,效果这样。

(defun +hs-adjust-block-end (hidden-beg)
  "Preserve the newline before the closing delimiter."
  (max hidden-beg
       (1- (line-beginning-position))))

(defun +hs-preserve-closing-line ()
  "Configure Hideshow to preserve the closing delimiter line."
  (setq-local hs-adjust-block-end-function
              #'+hs-adjust-block-end))

(add-hook 'java-ts-mode-hook #'+hs-preserve-closing-line)