Vterm completion for files, directories, command history and programs in Emacs

·

5 min read

This is a naive solution. Feel free to suggest alternative solutions or improvements.

gist.github.com/ram535/a2153fb86f33ecec587d..

The Goal

When pressing TAB in vterm terminal, we should get a list of suggestion. The suggestion list should either be a file, a directory, a history command or a program name.

What we need

This is the list of what we need for the solution.

  • emacs
  • vterm
  • bash

Step 1 - Get the list of program

If you go to the terminal and run:

compgen -c

You will get a list of all the programs under the PATH environment variable.

Let's get that list using elisp.

(shell-command-to-string "compgen -c")
;; "emacs\nnano\nneovim\nvim......."

This gives us a long string.

Let's split that long string into a list of string.

(split-string (shell-command-to-string "compgen -c") "\n" t )
;; ("emacs" "nano" "neovim" "vim".......)

Now we have a list of the programs of the system.

Step 2 - Choose a program from the list of programs

Now we can choose an item from that list of programs using the completing-read build-in emacs function.

(completing-read "Command " (split-string (shell-command-to-string "compgen -c") "\n" t ))

Step 3 - Send the chosen program to vterm

(vterm-send-string
 (completing-read "Command "
                  (split-string (shell-command-to-string "compgen -c") "\n" t )))

Step 4 - Get the list of files and directories in the CWD

If you go to the terminal and run:

compgen -f

You will get the list of files and directories in the current working directory (CWD).

Let's get that list using elisp.

(shell-command-to-string "compgen -f")
;; "Documents\nDownload\nMusic\nProjects......."

This gives us a long string.

Let's split that long string into a list of string.

(split-string (shell-command-to-string "compgen -f") "\n" t )
;; ("Documents" "Download" "Music" "Projects".......)

Now we have a list of files and directories of the CWD.

Step 5 - Choose a file or directory from the list of files and directories

(completing-read "Choose " (split-string (shell-command-to-string "compgen -f") "\n" t ))

Step 6 - Send the chosen file or directory to vterm

(vterm-send-string
 (completing-read "Choose "
                  (split-string (shell-command-to-string "compgen -f") "\n" t )))

Step 6.5 - There is a gotcha with getting the list of files and directories

The default-directory emacs variable is never update when we move to different directories in a vterm terminal.

That can be solve calling this function.

(defun vterm-directory-sync ()
  "Synchronize current working directory."
  (interactive)
  (when vterm--process
    (let* ((pid (process-id vterm--process))
           (dir (file-truename (format "/proc/%d/cwd/" pid))))
      (setq default-directory dir))))

The Step 14 has been update with this solution and everything should work as intended.

Step 7 - Get the list of bash history commands

Bash history commands are save in the .bash_history file.

Let's create a temporary buffer and insert the content of .bash_history file into it.

(with-temp-buffer
  (insert-file-contents "~/.bash_history")
  (split-string (buffer-string) "\n" t))

Let's split the content of the temporary buffer into a list of strings.

(with-temp-buffer
  (insert-file-contents "~/.bash_history")
  (split-string (buffer-string) "\n" t))

Now we have a list of bash command history.

Step 8 - Choose a command from the list of bash command history

(completing-read "History" (with-temp-buffer
                                (insert-file-contents "~/.bash_history")
                                (split-string (buffer-string) "\n" t)))

Step 9 - Send the chosen history command to vterm

(vterm-send-string
   (completing-read "History" (with-temp-buffer
                                (insert-file-contents "~/.bash_history")
                                (split-string (buffer-string) "\n" t))))

Step 10 - Combine the list of files, directories, history commands and programs into one list

Let's combine the lists we got from step 1, 4 and 7 into one list.

(let ((program-list (split-string (shell-command-to-string "compgen -c") "\n" t ))
      (file-directory-list (split-string (shell-command-to-string "compgen -f") "\n" t ))
      (history-list (with-temp-buffer
                      (insert-file-contents "~/.bash_history")
                      (split-string (buffer-string) "\n" t))))

  (append program-list file-directory-list history-list))

Let's make it a function.

(defun get-full-list ()
  (let ((program-list (split-string (shell-command-to-string "compgen -c") "\n" t ))
        (file-directory-list (split-string (shell-command-to-string "compgen -f") "\n" t ))
        (history-list (with-temp-buffer
                        (insert-file-contents "~/.bash_history")
                        (split-string (buffer-string) "\n" t))))

    (append program-list file-directory-list history-list)))

Step 11 - Delete duplicates (optional)

We are going to use -distinct function from the dash.el package.

(defun get-full-list ()
  (let ((program-list (split-string (shell-command-to-string "compgen -c") "\n" t ))
        (file-directory-list (split-string (shell-command-to-string "compgen -f") "\n" t ))
        (history-list (with-temp-buffer
                        (insert-file-contents "~/.bash_history")
                        (split-string (buffer-string) "\n" t))))

    (-distinct (append program-list file-directory-list history-list))))

Step 12 - Give suggestion for a partial word

Imagine we type:

em
  ^

^ is the position of the cursor. In this scenario we would like to have a suggestion of items that contain the letters "em".

If we read the documentation of the function completing-read.

(completing-read PROMPT COLLECTION &optional PREDICATE REQUIRE-MATCH INITIAL-INPUT HIST DEF INHERIT-INPUT-METHOD)

We can see that we can give an INITIAL-INPUT.

But how do we get an INITIAL-INPUT. That is where thing-at-point build-in emacs function comes in handy.

Let's see some examples:

world
  ^

world
     ^

If we call (thing-at-point 'word 'no-properties), in either example, it returns "world". Now can use (thing-at-point 'word 'no-properties) as out INITIAL-INPUT.

Let's go back to our scenario. We type "em" and evaluate the code below, it will give us suggestion of words that contain "em" from the the list we got in the step 10.

(completing-read "Choose: " (get-full-list) nil nil (thing-at-point 'word 'no-properties))

Let's make it a function.

(defun vterm-completion-choose-item ()
(completing-read "Choose: " (get-full-list) nil nil (thing-at-point 'word 'no-properties)))

Step 13 - Replace partial word with the chosen word

First we check if there is a word at point.

(when (thing-at-point 'word))

If it is a word, let's delete it.

(when (thing-at-point 'word)
(backward-kill-word 1))

WARNING (backward-kill-word) will not work in vterm. You have to use (vterm-send-meta-backspace) instead.

Let's insert the chosen item in a vterm terminal.

(defvar vterm-chosen-item (vterm-completion-choose-item))

(when (thing-at-point 'word)
(vterm-send-meta-backspace))

(vterm-send-string vterm-chosen-item)

Let's make it a function.

(defvar vterm-chosen-item)

(defun vterm-completion ()
  (interactive)
  (setq vterm-chosen-item (vterm-completion-choose-item))
  (when (thing-at-point 'word)
    (vterm-send-meta-backspace))
  (vterm-send-string vterm-chosen-item))

Step 14 - Solution

I use general.el for the keybindings and evil-mode.

(use-package vterm
  :config        
  (defun get-full-list ()
    (let ((program-list (split-string (shell-command-to-string "compgen -c") "\n" t ))
          (file-directory-list (split-string (shell-command-to-string "compgen -f") "\n" t ))
          (history-list (with-temp-buffer
                          (insert-file-contents "~/.bash_history")
                          (split-string (buffer-string) "\n" t))))

      (delete-dups (append program-list file-directory-list history-list))))

  (defun vterm-completion-choose-item ()
    (completing-read "Choose: " (get-full-list) nil nil (thing-at-point 'word 'no-properties)))

  (defun vterm-completion ()
    (interactive)
    (vterm-directory-sync)
   (let ((vterm-chosen-item (vterm-completion-choose-item)))
      (when (thing-at-point 'word)
         (vterm-send-meta-backspace))
      (vterm-send-string vterm-chosen-item)))

  (defun vterm-directory-sync ()
    "Synchronize current working directory."
    (interactive)
    (when vterm--process
      (let* ((pid (process-id vterm--process))
             (dir (file-truename (format "/proc/%d/cwd/" pid))))
        (setq default-directory dir))))

  :general
  (:states 'insert
           :keymaps 'vterm-mode-map
           "<tab>" 'vterm-completion))

Extra

Increase the bash history command and do not store duplicate items.

Add this in the .bashrc file.

export HISTSIZE=10000
export HISTFILESIZE=10000
export HISTCONTROL=ignoreboth:erasedups