暗无天日

=============>DarkSun的个人博客

在Eshell中快速跳转到常用目录

我在 之前的文章中 提到一种把目录加为书签以实现快速跳转的方法. 这个功能十分好用, 然而由于我用Eshell比较多,所以就像把这个功能移植到Eshell中.

如果只是把之前的shell脚本转换成elisp来做,其实蛮简单的,也已经有人这么做了: Bookmarking directories in Eshell.

但实际上Emacs本身就已经有了一套书签系统了(bookmark.el), 把Eshell与这套书签系统对接起来才是像是Emacs的风格吧.

现在我们就来尝试实现一下吧.

marks函数

该函数用于列出所有的书签及其指向的目录:

(require 'bookmark)

(defun eshell/marks ()
  (bookmark-maybe-load-default-file)
  (let* ((bookmarks (bookmark-maybe-sort-alist))
         (directory-bookmarks (remove-if-not (lambda (bookmark)
                                               (file-directory-p (bookmark-get-filename bookmark)))
                                             bookmarks))
         (show-bookmark-fn (lambda (bookmark)
                             (let ((name (bookmark-name-from-full-record bookmark))
                                   (filename (bookmark-get-filename bookmark)))
                               (format "%s      ->      %s" name filename)))))
    (mapconcat show-bookmark-fn directory-bookmarks "\n")))

首先是加载bookmark库

然而第二句的 (bookmark-maybe-load-default-file) 是让bookmark加载之前已经保存的书签信息.

在然后我们通过函数 (bookmark-maybe-sort-alist) 来获取书签列表,并只保留那些指向是目录的书签.

最后我们以 "书签名 -> 指向目录" 的格式输出书签信息就行了.

这里有几点需要注意一下:

  • 在eshell中执行的内部命令,其实是调用名为 eshell/命令 的函数,比如在eshell下执行 marks,那么就会调用这里定义的 eshell/marks 函数了
  • (bookmark-maybe-load-default-file) 这一句是必不可少的,若没有这句话,那么在执行任何bookmark命令之前,书签列表都是为空的.
  • eshell会将函数返回的最后结果作为是命令的输出(不是用message来输出的),所以最后我们使用了 mapconcat 来将字符串列表拼接成一个完整的字符串.

jump函数

jump函数接一个书签名作为参数,跳转到其指向的目录中:

(defun eshell/jump (bookmark-name)
  (bookmark-maybe-load-default-file)
  (let ((filename (bookmark-get-filename bookmark-name)))
    (if (file-directory-p filename)
        (eshell/cd filename)
      (error "%s is not a directory" bookmark-name))))

第一步依然是用 (bookmark-maybe-load-default-file) 加载书签信息.

然后使用 bookmark-get-filename 函数可以根据传入的书签名称找到所指向的路径.

最后判断一下指向的路径是否是目录,如果是目录就调用 eshell/cd 进入所指向的目录,否则就提示错误.

mark函数

mark函数接受一个书签名,并将当前目录加为书签:

(defun eshell/mark (&optional bookmark-name no-overwrite)
  (let ((buffer-file-name default-directory))
    (bookmark-set bookmark-name no-overwrite)))

mark函数这里其实做了点讨巧的事情.

bookmark库本身提供了一个 bookmark-set 函数用于添加书签(问我怎么找到的? 用C-h k C-x r m 查出来的).

但是很可惜,若你在eshell中执行bookmark-set这个命令会发现出错了,错误提示为 "Buffer not visiting a file or directory".

因为bookmark是用 buffer-file-name 作为书签的指向内容的, 而 eshell buffer中的 buffer-file-namenil.

不过这个问题也很好解决,用 let 临时指定 buffer-file-namedefaullt-directory 的指就好了(不得不说,动态作用域还是有好处的).

unmark函数

unmark函数接受一个书签名做参数,然后删掉指定的书签:

(defalias 'eshell/unmark 'bookmark-delete)

这就很简单了,直接使用 bookmark 库中的 bookmark-delete 函数就行了. 所以我这里只是用 defaliasbookmark-delete 定义个别名就OK了.

补全

最后是为jump函数添加补全功能. eshell使用pcomplete这个库来进行补全,至于怎么来写补全函数以后找时间再写另一片文章吧.

(defun pcmpl-bookmark-names (&optional name)
  "Return a list of directory bookmark names"
  (bookmark-maybe-load-default-file)
  (let* ((name (or name ""))
         (bookmarks (bookmark-maybe-sort-alist))
         (directory-bookmarks (cl-remove-if-not (lambda (bookmark)
                                               (file-directory-p (bookmark-get-filename bookmark)))
                                             bookmarks))
         (bookmark-names (mapcar #'bookmark-name-from-full-record directory-bookmarks)))
    (cl-remove-if-not (lambda (bookmark-name)
                     (string-prefix-p name bookmark-name))
                   bookmark-names)))

(defun pcomplete/jump ()
  "completion for `jump'"
  (while
      (pcomplete-here
       (pcmpl-bookmark-names (pcomplete-arg 'last)))))