DEV Community

aldama
aldama

Posted on

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

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

https://gist.github.com/ram535/a2153fb86f33ecec587d593c1c5e1623

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
Enter fullscreen mode Exit fullscreen mode

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......."
Enter fullscreen mode Exit fullscreen mode

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".......)
Enter fullscreen mode Exit fullscreen mode

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 ))
Enter fullscreen mode Exit fullscreen mode

Step 3 - Send the chosen program to vterm

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

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

If you go to the terminal and run:

compgen -f
Enter fullscreen mode Exit fullscreen mode

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......."
Enter fullscreen mode Exit fullscreen mode

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".......)
Enter fullscreen mode Exit fullscreen mode

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 ))
Enter fullscreen mode Exit fullscreen mode

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 )))
Enter fullscreen mode Exit fullscreen mode

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))))
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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)))
Enter fullscreen mode Exit fullscreen mode

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))))
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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)))
Enter fullscreen mode Exit fullscreen mode

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))))
Enter fullscreen mode Exit fullscreen mode

Step 12 - Give suggestion for a partial word

Imagine we type:

em
  ^
Enter fullscreen mode Exit fullscreen mode

^ 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)
Enter fullscreen mode Exit fullscreen mode

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
     ^
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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)))
Enter fullscreen mode Exit fullscreen mode

Step 13 - Replace partial word with the chosen word

First we check if there is a word at point.

(when (thing-at-point 'word))
Enter fullscreen mode Exit fullscreen mode

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

(when (thing-at-point 'word)
(backward-kill-word 1))
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Discussion (0)