Autocomplete for paths
The last week was a bit chaotic. Our dog Triss had TPLO surgery, so my days mostly consisted of holding her paw in the morning, working while Jean holds her paw, and then going to sleep. But I could sneak some fun project time in and started implementing autocomplete in my shell. Which is not even remotely complete, but it works for paths, which is something.
Initially I was a bit too clever trying to cache a lot of the information, but I opted to simply redo all the work, like reading a directory and deciding which completion candidates to have on each press of the tab key. I might revisit this later, but for now it is good enough.
Working on autocomplete also was a reminder how many things a regular shell does you usually do not think about, like expanding ~ to your home directory.
if strings.HasPrefix(c.cmd, "~") {
if home, err := os.UserHomeDir(); err == nil {
c.cmd = strings.Replace(c.cmd, "~", home, 1)
} else {
fmt.Fprintln(os.Stderr, err.Error())
}
}
The first thing to figure out is what the path is you want to pass to os.ReadDir and what the prefix is to filter suggestions. When you type commands/e you obviously only care about files and directories in commands which start with e. I probably should have used the operating systems path separator to keep the shell portable, but realistically this will not happen, so checking for / is fine.
comps := strings.Split(prefix, "/")
if strings.HasPrefix(prefix, "/") && len(comps) == 2 {
readDir = "/"
newPrefix = comps[len(comps)-1]
} else if len(comps) > 0 {
readDir = strings.Join(comps[:len(comps)-1], "/")
newPrefix = comps[len(comps)-1]
}
When you start managing your input buffer it feels like the easiest way to handle autocomplete is to simply replace the whole buffer and refresh the current line. I am certain only printing the characters that are new and incrementing the cursor index would be a bit more efficient, but so far I am aiming for "working", not perfect.
The last part is having a "completion index". Coming back to our commands directory. If there are three commands starting with e you obviously want to be able to iterate through all completion options. Repeatedly pressing tab increments the completion index. When any other button is pressed the index is reset. Also do not forget to reset the index when exceeding slice boundaries.
// round and round we go
if c.completionIndex >= len(c.pathList) {
c.completionIndex = 0
}
Progress
This seems to be working good enough. What I am still missing is autocomplete for paths within a command. Say dd if=/dev/random of=/taxes.pdf. If I want to switch /dev/random for /dev/null autocomplete would not work. But that’s a detail for later. The same goes for completing actual commands and binaries in $PATH and obviously built-ins, which I assume will be a bit easier.
One thing working on the unit tests for path completion I noticed is how much I miss neotests watch feature when not using Neovim. If I ever give Zed another try I might just work on a plugin mimicking this behaviour, it is just too convenient to not have it.
posted on June 21, 2026, 5:25 p.m. in golang, lazerbunny