Files
iris-context/commands/fs/zoxide.go
T
Kenny B 0b2221d5e9 feat(cd): optionally complete from zoxide's database
`cd` completes from FileGenerator, which reads one directory: the one you
are standing in. So `cd proj` only ever resolves when proj happens to be
a child of the current directory, and returns nothing at all otherwise --
including for directories visited constantly. That is the opposite of
what cd is normally reached for, and it reads as completion being broken
rather than as a scope limit.

zoxide.extend-cd routes `cd` through the generator `z` already uses, so
local children are offered first and zoxide's frecency database fills in
everything else, reachable by name from anywhere.

Off by default: it shells out to zoxide, and offering absolute paths from
outside the current directory is a real change in what `cd` proposes.

The query is also cached for two seconds. Generators run on every
keystroke and `zoxide query -l` is a subprocess costing ~11ms here, which
is the whole redraw budget; the list only changes when the user changes
directory, so the window costs nothing. This applies to `z` and `zi` too,
which were paying it per keystroke already.
2026-08-14 13:01:02 +03:00

126 lines
3.2 KiB
Go

// please note that zoxide also shows external suggestions
// at the end of the list on command mode
// they are the old directories that you have visited
// this is a feature, not a bug, and I want to keep it
package fs
import (
"bytes"
"context"
"os"
"os/exec"
"strings"
"sync"
"time"
"github.com/versenilvis/fuzzy"
"github.com/versenilvis/iris/spec"
)
func init() {
spec.Register(&spec.Spec{
Name: "z",
Description: "jump to directory",
MaxArgs: 0,
Generator: ZoxideGenerator(),
})
spec.Register(&spec.Spec{
Name: "zi",
Description: "jump to directory interactively",
MaxArgs: 0,
Generator: ZoxideGenerator(),
})
}
// zoxideCacheTTL bounds how stale the directory list may be. Generators run on
// every keystroke and `zoxide query -l` is a subprocess costing on the order of
// ten milliseconds, which is the entire latency budget for a suggestion redraw.
// The list only changes when the user changes directory, so a short window
// costs nothing in practice.
const zoxideCacheTTL = 2 * time.Second
var zoxideCache struct {
sync.Mutex
dirs []string
err error
fetched time.Time
}
// zoxideDirs returns zoxide's known directories, most frecent first.
func zoxideDirs() ([]string, error) {
zoxideCache.Lock()
defer zoxideCache.Unlock()
if !zoxideCache.fetched.IsZero() && time.Since(zoxideCache.fetched) < zoxideCacheTTL {
return zoxideCache.dirs, zoxideCache.err
}
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
out, err := exec.CommandContext(ctx, "zoxide", "query", "-l").Output()
zoxideCache.fetched = time.Now()
zoxideCache.err = err
zoxideCache.dirs = nil
if err != nil {
return nil, err
}
for line := range strings.SplitSeq(string(bytes.TrimSpace(out)), "\n") {
if line = strings.TrimSpace(line); line != "" {
zoxideCache.dirs = append(zoxideCache.dirs, line)
}
}
return zoxideCache.dirs, nil
}
func ZoxideGenerator() spec.GeneratorFunc {
return func(tokens []string, prefix string, partial string) []spec.Suggestion {
fullQuery := strings.Join(tokens[1:], " ")
localSuggestions := spec.FileGenerator("/")(tokens, prefix, fullQuery)
var zoxideSuggestions []spec.Suggestion
dirs, err := zoxideDirs()
if err == nil {
home, _ := os.UserHomeDir()
if fullQuery == "" {
limit := min(len(dirs), 20)
for i := range limit {
path := dirs[i]
display := strings.Replace(path, home, "~", 1)
zoxideSuggestions = append(zoxideSuggestions, spec.Suggestion{
Cmd: path,
Desc: display,
})
}
} else if !strings.Contains(fullQuery, "/") {
searcher := fuzzy.NewPlainSearcher(dirs)
matches := searcher.SearchWithScores(fullQuery, &fuzzy.SearchOptions{Limit: 10})
for _, m := range matches {
path := m.Str
display := strings.Replace(path, home, "~", 1)
zoxideSuggestions = append(zoxideSuggestions, spec.Suggestion{
Cmd: path,
Desc: display,
})
}
}
}
var finalResults []spec.Suggestion
seen := make(map[string]bool)
finalResults = append(finalResults, localSuggestions...)
for _, s := range zoxideSuggestions {
if !seen[s.Cmd] {
finalResults = append(finalResults, s)
seen[s.Cmd] = true
}
}
return finalResults
}
}