分享一下我用*scratch* 的姿势

最近经常在scratch buffer 中粘贴一段json ,以便将其格式化、或折叠部分json结构, 每次都要手动将其切换到json-ts-mode后 才能使用其中的快捷键 很不爽, 于是研究了 粘贴的时候自动根据粘贴的内容切换到相应的major-mode 代码如下:

另外一个就是每次kill scratch 这个buffer时 我会将其保存到cache目录下,以防万一需要找回之前kill掉的scratch buffer 。 一并放在这,供大家参考。

写这篇文章的时候 突然想到一个管理scratch 的思路(还没实现), 就是将 scratch 与 ~/.cache/emacs/scratch 这个文件进行关联 并用 git 管理这个目录, 每次kill scratch buffer的时候 将其主动写入文件执行 git add;git commit 等命令, 然后就可以利用 git timemachine 等package来遍历 scratch 的历史了。

(defun scratch-auto-set-major-mode (&optional arg)
  (when (and (string= (buffer-name) "*scratch*")
             (or (eq this-command 'yank)
                 (eq this-command 'meow-replace)))
    (set-auto-mode)))

(advice-add 'yank :after #'scratch-auto-set-major-mode)
(advice-add 'meow-replace :after #'scratch-auto-set-major-mode)

;; 将 *scratch* 内容写入指定目录下的备份文件
(defun backup-scratch ()
  (let* ((backup-file (format-time-string "scratch-%Y-%m-%d-%H-%M-%S.el"))
         (backup-path (concat my-scratch-backup-directory backup-file)))
    (with-current-buffer "*scratch*"
      (write-region (point-min) (point-max) backup-path nil 'silent))
    backup-file))

;; 删除指定目录下 3 天前的临时文件
(defun delete-old-scratch-backups ()
  (let ((max-age (* 3 24 60 60)) ; 3 天的时间戳
        (now (float-time)))
    (dolist (file (directory-files my-scratch-backup-directory t "scratch-.*\\.el$"))
      (when (and (file-regular-p file)
                 (> (- now (float-time (nth 5 (file-attributes file))))
                    max-age))
        (delete-file file)
        (message "Deleted old scratch backup: %s" file)))))

;; 定时执行删除任务
(run-with-timer 0 (* 24 60 60) #'delete-old-scratch-backups)

(defun vmacs-kill-buffer-dwim(&optional buf)
  (interactive)
  (with-current-buffer (or buf (current-buffer))
    (cond
     ((equal (buffer-name) "*scratch*")
      (backup-scratch)
      ;; (copy-region-as-kill (point-min)(point-max))
      (kill-this-buffer))
     ((and (featurep 'server)
           (boundp 'server-buffer-clients)
           server-buffer-clients)
      (save-buffer)
      (server-edit))
     ((derived-mode-p 'eshell-mode 'term-mode 'shell-mode 'vterm-mode)
      (kill-this-buffer)
      ;; (vterm-toggle-hide )
      ;; (vmacs-add-to-killed-file-list)
      ;; (centaur-tabs-forward-group)
      ;; (centaur-tabs-backward-group)
      )
     ((equal major-mode 'gnus-group-mode)
      (gnus-group-exit))
     ((equal major-mode 'gnus-summary-mode)
      (gnus-summary-exit))
     ((equal major-mode 'gnus-article-mode)
      (gnus-summary-update-info))
     ((equal major-mode 'magit-status-mode)
      (magit-mode-bury-buffer 16))
     ( (derived-mode-p 'magit-mode)
       (let ((process (magit-section-value-if 'process)))
         (if (and process
                  (eq (process-status process) 'run))
             (magit-mode-bury-buffer)
           (magit-mode-bury-buffer 4))))
     ( (derived-mode-p 'org-agenda-mode)
       (dolist (f org-agenda-files)
         (kill-buffer (get-file-buffer f)))
       (kill-this-buffer)
       )
     ( (derived-mode-p 'reb-mode)
       (call-interactively 'reb-quit))
     ( (derived-mode-p 'calc-mode)
       (call-interactively 'calc-quit))
     ( (derived-mode-p 'mu4e-view-mode)
       (call-interactively 'mu4e-view-quit))
     ( (derived-mode-p 'xwidget-webkit-mode)
       (call-interactively 'kill-this-buffer))
     ( (or (derived-mode-p 'special-mode)
           (string-prefix-p "*vc-git" (buffer-name ))
           (derived-mode-p 'compilation-mode))
       (if (get-buffer-process (current-buffer))
           (bury-buffer-and-window)
         (kill-buffer-and-window)))
     (t
      (message "kill buffer %s" (buffer-name ))
      (kill-this-buffer)))))

4 个赞

试试oboe.el

我平时用 scratch 也很多,我记得还有人在放弃用 emacs 的功能后因为非常想念 scratch buffer 还单独做了个产品

1 个赞

实现了上面提到的用git 管理*scratch* 的思路,即将 *scratch* 与 ~/scratch.el 这个文件进行关联,然后git管理~/scratch.el 这个文件。

之所以突然想到用git 管理,是因为 平时我已经在M-n M-p 绑定到我的另外两个命令 vcgit-next-revision vcgit-prev-revision 用于快速查看某个文件的历史版本 现在 scratch 即然也是file 了,自然也可以用git 管理后 也利用上git的强大功能。

之所以所择放到 ~/ 下 是希望 *scratch* 的default-directory 是home 目录,使用起来相对方便 但home 目录相对很大,用git 进行管理这么大的目录 会很慢, 不过可以通过默认忽略所有文件的方式来解决这个问题

~/.git/info/exclude 中加入 "*" 即可

我平时都是通过vmacs-kill-buffer-dwim 这个命令来kill-buffer ,只需要对*scratch* 特殊处理一下即可。

如此 想切到*scratch* 时 ,只需要用自带的scratch-buffer 命令, kill-buffer 时 处理git commit 操作。 想看*scratch* 历史,仍然是平时用到的M-n M-p ,没有太多使用上的心智负担, 也不用占用额外的按键

(global-set-key "\C-x\C-v" 'scratch-buffer)
(global-set-key (kbd "M-p") 'vcgit-prev-revision)
(global-set-key (kbd "M-n") 'vcgit-next-revision)

(setq-default
 inhibit-startup-screen t;隐藏启动显示画面
 initial-scratch-message nil;关闭 scratch 消息提示
 ;; initial-major-mode 'emacs-lisp-mode ;scratch init mode
 ;; initial-buffer-choice t                ;默认打开 scratch buffer
 ;; initial-buffer-choice "~/*scratch*"
 initial-major-mode #'(lambda()(emacs-lisp-mode)
                        (setq buffer-file-name "~/scratch.el")
                        (setq default-directory "~/")
                        (setq-local write-contents-functions #'scratch-write-contents))
                                    
)
(defun scratch-write-contents ()
  ;; 避免autosave总是提醒是否真的保存
  (with-current-buffer "*scratch*"
    (let ((txt (buffer-string)))
      (when buffer-file-name
        (require 'vc-git)
        (unless (file-exists-p "~/.git")
          ;; git init ad ~
          (vc-git-create-repo)
          (with-temp-buffer
            (insert "*")              ;default ignore all files in ~
            (append-to-file (point-min)(point-max) "~/.git/info/exclude")))
        (save-window-excursion
          (with-temp-file buffer-file-name
            (insert txt))
          (set-buffer-modified-p nil)
          (vc-git-register `(,(buffer-file-name)))
          (vc-git-command nil 'async buffer-file-name
                          "commit" "-m" "autosave" "-q"
                          ))))))

(defun vmacs-kill-buffer-dwim(&optional buf)
  (interactive)
  (with-current-buffer (or buf (current-buffer))
    (cond
     ((equal (buffer-name) "*scratch*")
      (save-buffer)
      (kill-this-buffer))
     ((and (featurep 'server)
           (boundp 'server-buffer-clients)
           server-buffer-clients)
      (save-buffer)
      (server-edit))
     ((derived-mode-p 'eshell-mode 'term-mode 'shell-mode 'vterm-mode)
      (kill-this-buffer)
      ;; (vterm-toggle-hide )
      ;; (vmacs-add-to-killed-file-list)
      ;; (centaur-tabs-forward-group)
      ;; (centaur-tabs-backward-group)
      )
     ((equal major-mode 'gnus-group-mode)
      (gnus-group-exit))
     ((equal major-mode 'gnus-summary-mode)
      (gnus-summary-exit))
     ((equal major-mode 'gnus-article-mode)
      (gnus-summary-update-info))
     ((equal major-mode 'magit-status-mode)
      (magit-mode-bury-buffer 16))
     ( (derived-mode-p 'magit-mode)
       (let ((process (magit-section-value-if 'process)))
         (if (and process
                  (eq (process-status process) 'run))
             (magit-mode-bury-buffer)
           (magit-mode-bury-buffer 4))))
     ( (derived-mode-p 'org-agenda-mode)
       (dolist (f org-agenda-files)
         (kill-buffer (get-file-buffer f)))
       (kill-this-buffer)
       )
     ( (derived-mode-p 'reb-mode)
       (call-interactively 'reb-quit))
     ( (derived-mode-p 'calc-mode)
       (call-interactively 'calc-quit))
     ( (derived-mode-p 'mu4e-view-mode)
       (call-interactively 'mu4e-view-quit))
     ( (derived-mode-p 'xwidget-webkit-mode)
       (call-interactively 'kill-this-buffer))
     ( (or (derived-mode-p 'special-mode)
           (string-prefix-p "*vc-git" (buffer-name ))
           (derived-mode-p 'compilation-mode))
       (if (get-buffer-process (current-buffer))
           (bury-buffer-and-window)
         (kill-buffer-and-window)))
     (t
      (message "kill buffer %s" (buffer-name ))
      (kill-this-buffer)))))

;;;###autoload
(defun vcgit-next-revision ()
  "Visit the next blob which modified the current file."
  (interactive)
  (let* ((buffname (buffer-name))
         (prev-buffer (current-buffer))
         (parent-buffer vc-parent-buffer)
         (fileset-arg (vc-deduce-fileset nil t))
         (backend (car fileset-arg))
         (filename (car (cadr fileset-arg)))
         rev next)
    (when parent-buffer
      (if (bound-and-true-p vc-buffer-revision)
          (setq rev vc-buffer-revision)
        (when (string-match "^\\([^~]+?\\)\\(?:\\.~\\([^~]+\\)~\\)?$" buffname)
          (setq rev (match-string 2 buffname))))
      (setq next (vc-call-backend backend 'next-revision
                                  filename rev))
      (kill-buffer prev-buffer)
      (if next
          (switch-to-buffer (vc-find-revision filename next))
        (find-file filename)
        (user-error "vcgit timemachine: You have reached the end of time")))))
;;;###autoload
(defun vcgit-prev-revision ()
  "Visit the prev blob which modified the current file."
  (interactive)
  (let* ((buffname (buffer-name))
         (cur-buffer (current-buffer))
         (parent-buffer vc-parent-buffer)
         (fileset-arg (vc-deduce-fileset nil t))
         (backend (car fileset-arg))
         (filename (car (cadr fileset-arg)))
         rev prev)
    (if (bound-and-true-p vc-buffer-revision)
        (setq rev vc-buffer-revision)
      (when (string-match "^\\([^~]+?\\)\\(?:\\.~\\([^~]+\\)~\\)?$" buffname)
        (setq rev (match-string 2 buffname))))
    (if rev
        (setq prev (vc-call-backend backend 'previous-revision
                                    filename rev))
      (setq prev (vc-working-revision filename)))
    (if prev
        (progn  (switch-to-buffer (vc-find-revision filename prev))
                (when parent-buffer (kill-buffer cur-buffer)))
      (user-error "vcgit timemachine: You have reached the beginning of time"))))

之所以所择放到 ~/ 下 是希望 scratch 的default-directory 是home 目录,使用起来相对方便 但home 目录相对很大,用git 进行管理这么大的目录 会很慢, 不过可以通过默认忽略所有文件的方式来解决这个问题

~/.git/info/exclude 中加入 "*" 即可

这样的思路有一些过于 hackish 了,而且把 HOME 目录设置成 .git 很容易有意想不到的副作用。因为很多应用都会 assume .git 是 project-root,这样你等于把你的 HOME 目录当成你的 project-root 了,这样对于你真正的 git project 就会形成嵌套 project。容易有意想不到的意外情况。

你可以在创建 scratch buffer 的时候写一个函数或者 advice 动态绑定 default-directory 为你的 HOME 目录。另外就是你可以把你的 scratch.el 的真实目录放在一个 cache 目录底下,然后 ~/scratch.el 可以是一个 symlink link 到你位于 cache 目录底下的 scratch.el文件里。

1 个赞

他这个确实不好. 不过 Git 目录是可以手动指定的, 不一定得是 .git.

我是用

国人项目,不知道作者在不在论坛里。

emacs启动时, 自动从~/.emacs.scratch文件加载内容到 *scratch*

emacs退出时, 将*scratch*内容保存到~/.emacs.scratch中

如果*scratch*内容有变化, 在idle time保存到~/.emacs.scratch

如果*scratch*内容没有变化, 每隔几分钟尝试保存, 没有变化时, 检查间隔不断变大