vterm-edit-command 在独立的 buffer 中编辑 vterm 当前命令行

感谢 @twlz0ne @jixiuf 的帮助

vterm-edit-command 用法是当你在 vterm 中按下 C-x C-e,会打开一个独立的 sh-mode buffer,编辑完毕之后按下 C-c C-c,就会把这个 buffer 中的内容复制到 vterm 中去。

需要注意的是,像下面代码的第一行会在你按下 C-c C-c 后会立刻执行的,因为换行的回车动作也被发送到 vterm 中去了。第二行则不会执行,因为我在代码里执行了 string-trim 删掉了首尾的空白字符。总之执行之前要小心一些危险命令,可能没有第二次修改的机会了。

echo 1 # 会立刻执行
echo 2 # 不会执行

下面的是代码,在 vterm 加载后执行即可。

(require 'subr-x)

(define-key vterm-mode-map (kbd "C-x C-e") 'vterm-edit-command-action)

(defun vterm-edit-command-action ()
  (interactive)
  (let* ((vterm-buffer (current-buffer))
         (begin-point (vterm--get-prompt-point))
         (end-point (point)))
    (setq vterm-edit-command--vterm-buffer vterm-buffer)
    (setq vterm-edit-command--begin-point begin-point)
    (setq vterm-edit-command--end-point end-point)
    (kill-ring-save begin-point end-point)
    (vterm-edit-command-buffer)))

(defun vterm-edit-command-commit ()
  (interactive)
  (let ((content (buffer-string)))
    (with-current-buffer vterm-edit-command--vterm-buffer
      (vterm-delete-region vterm-edit-command--begin-point vterm-edit-command--end-point)
      (vterm-send-string (string-trim content))))
  (vterm-edit-command-abort))

(defun vterm-edit-command-abort ()
  (interactive)
  (kill-buffer-and-window))

(defvar vterm-edit-command-mode-map
  (let ((keymap (make-sparse-keymap)))
    (define-key keymap (kbd "C-c C-c") #'vterm-edit-command-commit)
    (define-key keymap (kbd "C-c C-k") #'vterm-edit-command-abort)
    keymap))

(define-minor-mode vterm-edit-command-mode
  "Vterm Edit Command Mode")

(defun vterm-edit-command-buffer ()
  (let ((buffer (get-buffer-create "vterm-edit-command")))
    (with-current-buffer buffer
      (insert-buffer-substring
       vterm-edit-command--vterm-buffer
       vterm-edit-command--begin-point
       vterm-edit-command--end-point)
      (vterm--remove-fake-newlines)
      (set-text-properties (point-min) (point-max) nil)
      (goto-char (point-max))
      (insert "\n")
      (sh-mode)
      (vterm-edit-command-mode)
      (setq-local header-line-format
                  (substitute-command-keys
                   (concat "Edit, then "
                           (mapconcat
                            'identity
                            (list "\\[vterm-edit-command-commit]: Finish"
                                  "\\[vterm-edit-command-abort]: Abort"
                                  )
                            ", "))))
      (split-window-sensibly)
      (switch-to-buffer-other-window buffer))))

(provide 'vterm-edit-command)
;;; vterm-edit-command.el ends here
4 个赞

我现在不在电脑旁,晚些时候我会把完整代码放到 Github。

1 个赞

vterm 上游 push了一个新的commit ,可以尝试拉下最新代码,验证一下。

1 个赞

完整实现在这里:

效果:

separedit-feat-vterm

2 个赞

这里的 (let ((inhibit-read-only nil)) 是笔误还是有特别的意思?为 nil 则底下 (vterm--delete-region start end) 必然出错。

故意这么做的,真正的delete 只能由libvterm 触发, vterm–delete-region 内部 实际是调用 vterm-delete-char 等命令让底层去按字符一个个删除。加(inhibit-read-only nil) 就是为了防止误删导致不一致。

如果你删不了 必然是删了不该删的区域 可以尝试 M-: (vterm-delete-region (vterm--get-prompt-point) (point)) 我这边是能执行成功的

3 个赞

可以了,感谢!:pray:t2:

这儿换成

      (insert-buffer-substring
       vterm-edit-command--vterm-buffer
       vterm-edit-command--begin-point
       vterm-edit-command--end-point)
      (vterm--remove-fake-newlines)

对于 长行的处理 会好一些。

1 个赞

我加 no-properties 主要是想去掉 vterm 里面的高亮去使用 shell mode 里面的高亮。所以有没有办法把 fake-newlines 和高亮都去掉?

找到办法了,在后面加一句

(set-text-properties (point-min) (point-max) nil)
(defun vmacs-kill-buffer-dwim(&optional buf)
  (interactive)
  (save-buffer)
  (server-edit))

(defun zsh-find-file-hook()
  (when (string-prefix-p "/tmp/zsh" (buffer-file-name))
    (setq truncate-lines nil)
    (local-set-key (kbd "C-c C-k") #'server-edit-abort)
    (local-set-key (kbd "C-c C-c") #'vmacs-kill-buffer-dwim)))

(add-hook #'sh-mode-hook #'zsh-find-file-hook)

(define-key vterm-mode-map (kbd "C-x C-e") #'(lambda () (interactive) (vterm-send-string (kbd "C-x C-e"))))
(define-key vterm-mode-map (kbd "C-c e")  #'(lambda () (interactive) (vterm-send-string (kbd "C-x C-e"))))
(define-key vterm-mode-map (kbd "C-c '")  #'(lambda () (interactive) (vterm-send-string (kbd "C-x C-e"))))
(setq-default auto-mode-alist (append '(("zsh" . sh-mode)) auto-mode-alist))

大段内容的时候 vterm-delete-region 好像太慢了, 感觉还不如 zsh 原生C-xC-e 后使用emacsclient打开快。供参考。

不过现在vterm 通过 evil-collection 与evil 集成的已经很好了,常见的d c x 等操作,都能使用,基本也用不上这个。

还有一个思路,来应对vterm-delete-region 直接vterm-send-C-c (发送ctrl-c) 丢弃原内容,在一个全新的prompt 上展示修改后的内容

(defun vterm-edit-command-commit ()
  (interactive)
  (let ((content (buffer-string)))
    (with-current-buffer vterm-edit-command--vterm-buffer
      (vterm-send-C-c)
      (vterm-send-string content)))
  (vterm-edit-command-abort))
1 个赞

感觉你这个更直接。 这个 C-x C-e 在没有 emacsclient 的情况下,能在当前 vterm 所在的 emacs 打开么?

(define-key vterm-mode-map (kbd "C-x C-e") 'vterm-edit-command-action)
(define-key vterm-mode-map (kbd "C-c e") 'vterm-edit-command-action)

(defun vterm-edit-command-action ()
  (interactive)
  (let* ((delete-trailing-lines t)
         (vtermbuf (current-buffer))
         (begin (vterm--get-prompt-point))
         (buffer (get-buffer-create "vterm-edit-command"))
         (n (length (filter-buffer-substring begin (point))))
         foreground
         (content (filter-buffer-substring
                   begin (point-max))))
    (with-current-buffer buffer
      (setq vterm-edit-vterm-buffer vtermbuf)
      (erase-buffer)
      (insert content)
      (delete-trailing-whitespace)
      (goto-char (1+ n))
      ;; delete zsh auto-suggest candidates
      (setq foreground (plist-get (get-text-property (point) 'font-lock-face) :foreground ))
      (when (equal foreground  (face-background 'vterm-color-black nil 'default))
        (delete-region (point) (point-max)))
      (sh-mode)
      (vterm-edit-command-mode)
      (evil-insert-state)
      (setq-local header-line-format
                  (substitute-command-keys
                   (concat "Edit, then "
                           (mapconcat
                            'identity
                            (list "\\[vterm-edit-command-commit]: Finish"
                                  "\\[vterm-edit-command-abort]: Abort"
                                  )
                            ", "))))
      (split-window-sensibly)
      (switch-to-buffer-other-window buffer)))
  )

(defun vterm-edit-command-commit ()
  (interactive)
  (let ((delete-trailing-lines t)
        content)
    (delete-trailing-whitespace)
    (goto-char (point-max))
    (when (looking-back "\n") (backward-delete-char 1))
    (setq content (buffer-string))
    (with-current-buffer vterm-edit-vterm-buffer
      (vterm-send-key "a" nil nil t)
      (vterm-send-key "k" nil nil t t)
      (unless (vterm--at-prompt-p)
        (vterm-send-C-c))
      (vterm-send-string content)))
  (vterm-edit-command-abort))

(defun vterm-edit-command-abort ()
  (interactive)
  (kill-buffer-and-window))

(defvar vterm-edit-command-mode-map
  (let ((keymap (make-sparse-keymap)))
    (define-key keymap (kbd "C-c C-c") #'vterm-edit-command-commit)
    (define-key keymap (kbd "C-c C-k") #'vterm-edit-command-abort)
    keymap))

(define-minor-mode vterm-edit-command-mode
  "Vterm Edit Command Mode")

依据弃用vterm-delete-region的思路改进了一版, 这版与楼主思路不同的地方是, 把从(vterm--get-prompt-point)(point-max) 的所有内容 copy 到新buffer,然后在新buffer把末尾的所有空行干掉,这样相当于把所有内容copy 到新buffer, 另一个增强点是 把光标在vterm的相对位置带到了 新buffer

1 个赞