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
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
Top comments (0)