Emacs 是否能实现类似 VS Code 或者 NeoVim 的滚动条装饰

我在用 VS Code / NeoVim 的时候,一直觉得他们的滚动条装饰功能非常好用(在滚动条上添加标记,例如诊断信息等):

Emacs 人习惯在 mode-line 上放各种 indicator:diagnostics,isearch,smerge,marks 用来标记这个文件有哪些 indicator 起效,但是在我看来无论在美观度还是功能性层面,都是滚动条装饰更好:

  1. modeline 非常拥挤,使用滚动条装饰可以缓解其压力
  2. 滚动条装饰能在一片区域展示多个信息,减少了信息获取的成本(只需要瞄一眼就能看到全部indicators)
  3. 滚动条装饰提供了更多信息:例如 滚动条的长度暗示了文件的大小,滚动条的位置暗示了所处文件的位置(由此可以删掉 modeline 上的文件大小提示和文件位置提示);isearch 的结果集中在哪片区域,Git 状态显示了文件的哪一个部分被修改……

但是目前 emacs 的 scrollbar 使用了系统默认的图形框架,不能实现这个,我想知道如果想要这个功能的话,有什么好的实现机制吗

2 个赞

如果有识别滚动的hook的话,可以实现,可以参考 [POC] Emacs buffer 文本块滚动效果实现 。本质是上是在每次滚动时更新滚动条的文本属性。

为什么滚动条有文本属性,是用 fringer 实现的吗

就是在一个空格字符上设置 display 的 space属性,再反转一下颜色。放在哪里都可以,本质都是文本。

你要的是这个?其实 GitHub - LuciusChen/blame-reveal: A contextual, high-performance Git blame UI for Emacs. 也是类似的机制。用fringes

有两个问题:

  1. 对于现有的 buffer 我该怎么插入这个空格,似乎不太可行,但是似乎可以用 overlay 或者 fringer模拟
  2. 滚动的时候刷新不会有大量性能开销吗

具体我没研究过,放在行尾似乎不行,你说的fringe应该可以。性能开销还好。每次只改变滚动条开始和结尾位置的文本属性(改个颜色)基本没什么开销。你可以看看etaf滚动的效果是不是你想要的,如果是的话就可以实现。

为啥你觉得emacs的侧边没有类似的显示?eglot/bookmark 其实都有的,关键看你想做什么,如果是标记就fringes,如果文本多就margin。

关键是这几个东西都是和“行”绑定的,每次滚动都要刷新位置

感觉minimap之类符合你的想法,不过要改造成你想要的形状。

try diff-hl.el

利用 right-margin 不知道是否可以,这是 AI 提供的一个 MVP 代码:

;; file: scroll-decor.el

(defgroup scroll-decor nil
  "Scrollbar-like decorations using right margin."
  :group 'convenience)

(defface scroll-decor-viewport '((t :background "#666666"))
  "Face for current viewport indicator.")

(defface scroll-decor-diagnostic '((t :background "#e57373"))
  "Face for Flymake diagnostics.")

(defface scroll-decor-isearch '((t :background "#64b5f6"))
  "Face for isearch hits.")

(defvar-local scroll-decor--overlays nil)
(defvar scroll-decor-margin-width 1)

(defun scroll-decor--set-margin ()
  (set-window-margins (selected-window)
                      (car (window-margins))
                      scroll-decor-margin-width))

(defun scroll-decor--clear ()
  (mapc #'delete-overlay scroll-decor--overlays)
  (setq scroll-decor--overlays nil))

(defun scroll-decor--add (line face)
  (let* ((pos (save-excursion (goto-char (point-min))
                              (forward-line (max 0 (1- line)))
                              (point)))
         (ov (make-overlay pos pos)))
    (overlay-put ov 'scroll-decor t)
    (overlay-put ov 'priority 10000)
    (overlay-put ov 'before-string
                 (propertize " " 'display
                             `(margin right-margin
                                      ,(propertize " " 'face face))))
    (push ov scroll-decor--overlays)))

(defun scroll-decor--line-count ()
  (max 1 (count-lines (point-min) (point-max))))

(defun scroll-decor--normalize (pos)
  "Map buffer position POS to a line number along the window height."
  (let* ((total (scroll-decor--line-count))
         (line (line-number-at-position pos t))
         (win-lines (max 1 (window-body-height)))
         (y (max 1 (min win-lines
                        (round (* (/ (float line) total) win-lines))))))
    y))

(defun scroll-decor--viewport ()
  (let* ((start (window-start))
         (end   (window-end nil t)))
    (list (scroll-decor--normalize start)
          (scroll-decor--normalize end))))

(defun scroll-decor--collect-flymake ()
  (when (bound-and-true-p flymake-mode)
    (mapcar (lambda (d) (flymake--diag-beg d))
            (flymake-diagnostics))))

(defun scroll-decor--collect-isearch ()
  (when (and isearch-mode isearch-string)
    (save-excursion
      (goto-char (point-min))
      (let (hits)
        (while (search-forward isearch-string nil t)
          (push (match-beginning 0) hits))
        hits))))

(defun scroll-decor--render ()
  (scroll-decor--clear)
  (scroll-decor--set-margin)
  ;; viewport bar(用起止两点各画一个,视觉上是顶/底两个刻度)
  (pcase-let ((`(,y1 ,y2) (scroll-decor--viewport)))
    (scroll-decor--add y1 'scroll-decor-viewport)
    (scroll-decor--add y2 'scroll-decor-viewport))
  ;; diagnostics
  (dolist (pos (scroll-decor--collect-flymake))
    (scroll-decor--add (scroll-decor--normalize pos) 'scroll-decor-diagnostic))
  ;; isearch hits(可做采样以避免太密集)
  (dolist (pos (scroll-decor--collect-isearch))
    (scroll-decor--add (scroll-decor--normalize pos) 'scroll-decor-isearch)))

(defun scroll-decor--update (_win _start)
  (scroll-decor--render))

;;;###autoload
(define-minor-mode scroll-decor-mode
  "Simulate VSCode-like scrollbar decorations in right margin."
  :lighter " ⎯"
  (if scroll-decor-mode
      (progn
        (add-hook 'window-scroll-functions #'scroll-decor--update nil t)
        (add-hook 'post-command-hook #'scroll-decor--render nil t)
        (add-hook 'flymake-diagnostic-functions
                  (lambda (&rest _) (scroll-decor--render)) nil t)
        (scroll-decor--render))
    (remove-hook 'window-scroll-functions #'scroll-decor--update t)
    (remove-hook 'post-command-hook #'scroll-decor--render t)
    (scroll-decor--clear)))

holo-layer 插件应该可以实现的比VSCode还要帅气