求助在实现自己简单的minibuffer补全需求中遇到的问题

具体思路: 通过自带的abbrev-mode,在abbrev_defs里面设定四个字符作为严格的缩写词,缩写内容为次常用命令和dired复制的常用目录的绝对路径

"mate" "/home/mate/.emacs.d/"
"test" "eval-defun"

表现行为: 输入缩写词之后,自动用expand-abbrev展开,再调用exit-minibuffer完成

代码如下:

(setq abbrev-mode t)
(setq abbrev-file-name "~/.emacs.d/temp/abbrev_defs"

(defun four-chars ()
  (when (and (minibufferp)
             (= (length (minibuffer-contents)) 4))
    (abbrev-mode 1)
    (expand-abbrev)
    (exit-minibuffer)))

(defun custom-execute-extended-command ()
  (interactive)
  (add-hook 'post-command-hook 'four-chars)
  (execute-extended-command nil)
  (remove-hook 'post-command-hook 'four-chars))

(defun custom-dired-do-copy ()
  (interactive)
  (add-hook 'post-command-hook 'four-chars)
  (dired-do-copy nil)
  (remove-hook 'post-command-hook 'four-chars))

存在问题:

  1. 在上面两个custom-函数执行的时候,不管是不是缩略词,都会在四个字符之后自动执行,我只能尽可能把自己用到的命令和路径都写进去,算是一种掩耳盗铃式的解决方式
  2. 当使用那些会自动调用minibuffer的命令时候,包括但不限于replace-string,isearch等等,我的解决方案是使用major-mode-map里面绑定的单键快捷键,如果是通过custom-execute-extended-command来调用,就会继承四个字符自动调用的操作,没法用了,不知道怎么解决
  3. (custom-)dired-do-copy默认执行之后会插入当前路径,但是我缩写词使用的是绝对路径,所以必须把这路径删除
(advice-add 'dired-do-copy :after #'delete-backward-sentence)

这个并没有效果,有人指点我说after之后要加一个定时器,但是我不知道怎么弄,故求助这个,是刚需

我是一个纯菜鸟,这也是一个功能很简单的minibuffer补全,如果有和我类似这样需求的朋友们可以尝试一下,最好emacs -Q来测试

最后就是感谢所有提出建议和解决方案的朋友们,非常感谢大家


(setq abbrev-mode t)
(setq abbrev-file-name "~/.emacs.d/temp/abbrev_defs")
(setq save-abbrevs t)


(defun is-our-abbrev-p (str)
  "检查输入的字符串是否是我们定义的缩写词之一"
  (and (= (length str) 4)  ; 确保长度为4
       (assoc str abbrev-table-name-list)))  ; 检查是否在缩写表中

;; 改进的 four-chars 函数
(defun four-chars ()
  "只在输入的是已定义的缩写词时才展开并退出"
  (when (and (minibufferp)
             (is-our-abbrev-p (minibuffer-contents)))
    (abbrev-mode 1)
    (expand-abbrev)
    (exit-minibuffer)))

;; 创建一个通用的 minibuffer 包装函数
(defun with-abbrev-completion (orig-fun &rest args)
  "为函数添加缩写补全功能的包装器"
  (unwind-protect
      (progn
        (add-hook 'post-command-hook #'four-chars)
        (apply orig-fun args))
    (remove-hook 'post-command-hook #'four-chars)))

;; 包装 execute-extended-command
(defun custom-execute-extended-command ()
  (interactive)
  (with-abbrev-completion #'execute-extended-command nil))

;; 包装 dired-do-copy
(defun custom-dired-do-copy ()
  (interactive)
  (with-abbrev-completion #'dired-do-copy nil))

;; 修复 dired-do-copy 后的路径问题
(defun clear-minibuffer-after-delay ()
  "清除 minibuffer 中的内容"
  (when (minibufferp)
    (delete-region (minibuffer-prompt-end) (point-max))))

(advice-add 'dired-do-copy
            :after
            (lambda (&rest _)
              (run-with-timer 0 nil #'clear-minibuffer-after-delay)))

;; 为特定命令禁用缩写补全
(defvar commands-without-abbrev
  '(replace-string isearch-forward isearch-backward)
  "不需要缩写补全的命令列表")

(advice-add 'execute-extended-command
            :around
            (lambda (orig-fun &rest args)
              (let ((this-command (car (where-is-internal 
                                      (intern (completing-read 
                                             "M-x " obarray 'commandp t))
                                      nil t))))
                (if (memq this-command commands-without-abbrev)
                    (apply orig-fun args)
                  (apply #'with-abbrev-completion orig-fun args)))))
1 个赞

首先非常感谢这位大佬的方案

其次,我刚使用emacs -Q来测试这段代码,只是多了(require 'dired),以便把c和o绑定到两个custom命令上面,同时打开了debug,但是在执行custom-execute-extended-command和custom-dired-do-copy并没有出现预期的结果,还是默认的行为,整个过程也没有报错,不知道为什么

abbrev_defs里面我只写了minibuffer-mode-abbrev-table

是不是我哪里没用对,大佬指点一下

不好意思,我给出的配置不够严谨。完全理解错你的意思了。

以下是跟 AI 讨论后,设置一个生效时间的配置,但我未经过测试:

(defvar my-abbrev-timer nil
  "Timer for delayed abbrev expansion.")

(defun my-maybe-expand-abbrev ()
  "当输入停顿时检查并展开缩写"
  (when (and (minibufferp)
             (= (length (minibuffer-contents)) 4)
             (abbrev-symbol (minibuffer-contents)))
    (abbrev-mode 1)
    (expand-abbrev)
    (exit-minibuffer)))

(defun my-check-abbrev ()
  "设置一个timer来检查缩写"
  (when my-abbrev-timer
    (cancel-timer my-abbrev-timer))
  (setq my-abbrev-timer 
        (run-with-idle-timer 0.5 nil #'my-maybe-expand-abbrev)))

;; 修改原来的函数
(defun custom-execute-extended-command ()
  (interactive)
  (add-hook 'post-command-hook 'my-check-abbrev)
  (execute-extended-command nil)
  (remove-hook 'post-command-hook 'my-check-abbrev))
1 个赞

我最初在minibuffer中实现一个简单补全(单词和代码段)用的是hydra按键绑定的办法, 现在不这么写了,但因为惯性原因,偶尔还会用到此办法。 比如说按Isql触发输入下面的php代码然后格式化好然后把光标移到中间的空行处

foreach($dbs[0]->query($sql,\PDO::FETCH_ASSOC) as $f=>$v){
  
}

代码发出来供大家参考

(defhydra hydra-key-i()
  ("sql" (progn ;
      (insert "foreach($dbs[0]->query($sql,\\PDO::FETCH_ASSOC) as $f=>$v){\n\n}")
      (indent-for-tab-command)(previous-line)(indent-for-tab-command)(previous-line)(indent-for-tab-command) (next-line)) 
      "php查询sql" :exit t)
  ("c" nil "取消"))
(global-set-key (kbd "I") 'hydra-key-i/body)

总之随着对elisp的不断了解就会发现以前遇到的问题还有很多自己不知道的(更好的)解决办法

1 个赞

该方案完美的解决了前两个问题

针对第三个问题

(defun clear-minibuffer-after-delay ()
  "清除 minibuffer 中的内容"
  (when (minibufferp)
    (delete-region (minibuffer-prompt-end) (point-max))))

(advice-add 'dired-do-copy
            :after
            (lambda (&rest _)
              (run-with-timer 0 nil #'clear-minibuffer-after-delay)))

(defun custom-dired-do-copy ()
  (interactive)
  (add-hook 'post-command-hook 'my-check-abbrev)
  (dired-do-copy nil)
  (remove-hook 'post-command-hook 'my-check-abbrev))

仍然显示默认路径,并没有自动删除,不知道是什么原因

而且使用custom-dired-do-copy报错,但是文件却是复制成功了

debugger enter--lisp error:
(wrong-number-of-arguments (lambda nil (run-with-timer 0.3 nil #'(lambda nil (delete-backward-sentence)))) 1)
(lambda nil (run-with-timer 0.3 nil #'(lambda nil (delete-backward-sentence))))(nil)
apply((lambda nil (run-with-timer 0.3 nil #'(lambda nil (delete-backward-sentence)))) nil)
dired-do-copy(nil)
custom-dired-do-copy()
funcall-interactively(custom-dired-do-copy)
call-interactively(custom-dired-do-copy nil nil)
command-execute(custom-dired-do-copy)

感谢分享

你这个实现应该是对buffer的补全,如果我猜对了的话

我的这个需求就是针对minibuffer输入次常用的命令的补全,最常用的都已经针对日常模式定义了单键快捷键来操作,其次是对dired复制的路径补全,目的就只有这两个

只要在abbrev的基础上稍微增加几个函数就能够实现半自动化,让我自己节约了很多时间,也避免了输入关键字之后按tab或者上下键选择候选和复制文件手动切换目录的麻烦

在此之前使用的是ivy三件套,在长期使用过程中发现自己的命令就集中在swiper搜索和counsel-rg,其它的功能根本没用过,就想找替代方案,最后从论坛找到了isearch-mb来代替swiper,deadgrep来代替counsel-rg,然后无意中看到aggressive-completion,尝试了一下觉得自动补全关键字很有意思,但是容易操作失误,就联想到abbrev,才有了这个想法

没太理解。你可以看看这种 prefix 的方法。来自官方手册。

You may wish to expand an abbrev and attach a prefix to the expansion; for example, if ‘cnst’ expands into ‘construction’, you might want to use it to enter ‘reconstruction’. It does not work to type ‘recnst’, because that is not necessarily a defined abbrev. What you can do is use the command ‘M-‘’ (‘abbrev-prefix-mark’) in between the prefix ‘re’ and the abbrev ‘cnst’. First, insert ‘re’. Then type ‘M-’’; this inserts a hyphen in the buffer to indicate that it has done its work. Then insert the abbrev ‘cnst’; the buffer now contains ‘re-cnst’. Now insert a non-word character to expand the abbrev ‘cnst’ into ‘construction’. This expansion step also deletes the hyphen that indicated ‘M-'’ had been used. The result is the desired ‘reconstruction’.

就是执行custom-do-dired-copy的时候,默认还是有路径,然后我手动删除路径,在输入缩写词,报错,但是我查看目标路径,这个文件已经被复制过来了

完全没听懂。可能你要自行探索一下。

恩好的,非常感谢你的帮忙

经过@yibie老哥和我的好友不断指点,最终解决了这三个问题

这是最终代码,可能写得不规范:

(setq abbrev-mode t)
(setq abbrev-file-name "~/.emacs.d/temp/abbrev_defs")
(setq save-abbrevs t)

(defvar my-abbrev-timer nil
  "Timer for delayed abbrev expansion.")

(defun my-maybe-expand-abbrev ()
   (when (and (minibufferp)
              (eq (char-before) ?/)
              (delete-region (line-beginning-position) (point)))
   (when (and (minibufferp) 
              (= (length (minibuffer-contents)) 4)
              (abbrev-symbol (minibuffer-contents)))
     (abbrev-mode 1) 
     (expand-abbrev) 
     (exit-minibuffer)))

(defun my-check-abbrev ()
  "设置一个timer来检查缩写"
  (when my-abbrev-timer
    (cancel-timer my-abbrev-timer))
  (setq my-abbrev-timer 
        (run-with-idle-timer 0 nil #'my-maybe-expand-abbrev)))

(defun custom-execute-extended-command ()
  (interactive)
  (unwind-protect
      (progn
        (add-hook 'post-command-hook 'my-check-abbrev)
        (execute-extended-command nil)
        (remove-hook 'post-command-hook 'my-check-abbrev))))

(defun custom-dired-do-copy ()
  (interactive)
  (unwind-protect
      (progn
        (add-hook 'post-command-hook 'my-check-abbrev)
        (dired-do-copy nil)
        (remove-hook 'post-command-hook 'my-check-abbrev))))

再次感谢解决问题期间给予我帮助的所有人

1 个赞

虽然我没太看懂你的需求,但是这些 add-hook/remove-hook 最好用 (unwind-protect...)包裹起来,#2 楼已经给了示范。

1 个赞

非常感谢,我刚开始接触这个,很多都不明白,你的提醒我记住了,再次感谢

1 个赞

就是为了简化在M-x里面输入命令的过程

常用的命令都绑定到了各自主模式的快捷键里面,剩下的不好绑定或者不太常用的命令一般都是先输入关键字,然后选择补全项之后回车执行,我这样的功能就直接把这些命令的执行过程去掉了选择补全和回车执行两个步骤,直接一步到位

dired-do-copy也是一样的目的,自动化了切换目标文件夹和回车执行的操作

就是很简单的两个功能,唯一的好处就是节省时间