mirror of
https://github.com/patriceckhart/zot.git
synced 2026-06-26 13:26:33 +02:00
feat(tui): fuzzy @-file matching with toggleable recursive search
The @-mention file picker previously did a plain case-insensitive substring match within a single directory, only reachable nesting via arrow-key drill-down. - Rank matches with sahilm/fuzzy (pinned v0.1.1 to avoid the go 1.24.5 directive in v0.1.2, which would exceed CI's Go 1.23). - Add a recursive mode that walks the whole project tree below cwd, matching cwd-relative paths (e.g. @foobar finds src/foo/bar.go), skipping heavy dirs (.git, node_modules, ...) and bounded by entry and depth caps. Arrow drill-down is disabled in this mode. - Persist as recursive_file_suggest in config.json, surfaced as a /settings checkbox, plumbed through SettingsStore/InteractiveConfig/ cli. Toggling live flips the picker without a restart. - Tests for fuzzy ranking, recursive cross-dir match, heavy-dir pruning, and cache reset on toggle.
This commit is contained in:
parent
15f76e0fcd
commit
7ac6034d1d
8 changed files with 298 additions and 14 deletions
4
go.mod
4
go.mod
|
|
@ -6,12 +6,14 @@ require (
|
|||
github.com/alecthomas/chroma/v2 v2.23.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mattn/go-runewidth v0.0.16
|
||||
github.com/sahilm/fuzzy v0.1.1
|
||||
golang.org/x/image v0.18.0
|
||||
golang.org/x/sys v0.26.0
|
||||
golang.org/x/term v0.25.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
golang.org/x/image v0.18.0 // indirect
|
||||
)
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -10,10 +10,14 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
|
||||
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
|
|
|
|||
|
|
@ -944,6 +944,7 @@ func runInteractive(ctx context.Context, args Args, version string) error {
|
|||
Theme: theme,
|
||||
InlineImagesEnabled: initialCfg.InlineImagesEnabled,
|
||||
AutoSwarmEnabled: initialCfg.AutoSwarmEnabled,
|
||||
RecursiveFileSuggest: initialCfg.RecursiveFileSuggest,
|
||||
ThemeName: initialCfg.Theme,
|
||||
ExtensionThemes: extMgr.ThemeOptions,
|
||||
AutoSwarmSystemAddendum: AutoSwarmSystemAddendum,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,12 @@ type Config struct {
|
|||
// default; nil/missing means disabled. Toggle from /settings.
|
||||
AutoSwarmEnabled *bool `json:"auto_swarm_enabled,omitempty"`
|
||||
|
||||
// RecursiveFileSuggest controls the @-mention file picker. When true
|
||||
// the picker fuzzy-searches the whole project tree below the working
|
||||
// directory; nil/missing/false keeps the default directory-by-
|
||||
// directory browse. Toggle from /settings.
|
||||
RecursiveFileSuggest *bool `json:"recursive_file_suggest,omitempty"`
|
||||
|
||||
// LastChangelogShown is the version whose release-notes
|
||||
// dialog the user has already seen. When the running binary's
|
||||
// version differs, the next interactive run shows the
|
||||
|
|
|
|||
|
|
@ -8,9 +8,36 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sahilm/fuzzy"
|
||||
|
||||
"github.com/patriceckhart/zot/packages/tui"
|
||||
)
|
||||
|
||||
// recursiveScanLimits bound the recursive walk so the picker stays
|
||||
// responsive in very large repos. Hitting either cap stops the walk
|
||||
// early; the entries gathered so far are still searchable.
|
||||
const (
|
||||
maxRecursiveEntries = 5000
|
||||
maxRecursiveDepth = 12
|
||||
)
|
||||
|
||||
// recursiveSkipDirs are directory names never descended into during a
|
||||
// recursive scan. They dominate the entry budget without being useful
|
||||
// @-mention targets.
|
||||
var recursiveSkipDirs = map[string]bool{
|
||||
".git": true,
|
||||
"node_modules": true,
|
||||
".venv": true,
|
||||
"venv": true,
|
||||
"vendor": true,
|
||||
"target": true,
|
||||
"dist": true,
|
||||
"build": true,
|
||||
".idea": true,
|
||||
".vscode": true,
|
||||
"__pycache__": true,
|
||||
}
|
||||
|
||||
// fileSuggester provides an @-triggered file/directory picker popup.
|
||||
// Type "@" followed by an optional filter to list files in the working
|
||||
// directory. Arrow up/down navigate, enter selects, esc cancels.
|
||||
|
|
@ -20,8 +47,12 @@ type fileSuggester struct {
|
|||
lastMatches []fileEntry
|
||||
cwd string // project root
|
||||
browseRel string // relative path from cwd we're currently browsing ("" = cwd itself)
|
||||
cachedDir string // absolute directory we last scanned
|
||||
cachedAll []fileEntry
|
||||
// recursive enables a whole-tree fuzzy search instead of
|
||||
// directory-by-directory browsing. Mirrors the persisted
|
||||
// recursive_file_suggest setting; toggled live from /settings.
|
||||
recursive bool
|
||||
cachedDir string // absolute directory we last scanned
|
||||
cachedAll []fileEntry
|
||||
// cachedMTime is the mtime of cachedDir at the time of the scan.
|
||||
// scan() compares the current mtime against this on every call and
|
||||
// re-reads the directory if it has changed, so files or folders
|
||||
|
|
@ -49,6 +80,20 @@ func (s *fileSuggester) SetCWD(cwd string) {
|
|||
}
|
||||
}
|
||||
|
||||
// SetRecursive toggles whole-tree fuzzy search. Switching modes drops
|
||||
// the cache and resets the browse position so the next render reflects
|
||||
// the new mode immediately.
|
||||
func (s *fileSuggester) SetRecursive(on bool) {
|
||||
if s.recursive == on {
|
||||
return
|
||||
}
|
||||
s.recursive = on
|
||||
s.browseRel = ""
|
||||
s.cachedDir = ""
|
||||
s.cachedAll = nil
|
||||
s.cursor = 0
|
||||
}
|
||||
|
||||
// browseDir returns the absolute directory currently being browsed.
|
||||
func (s *fileSuggester) browseDir() string {
|
||||
if s.browseRel == "" {
|
||||
|
|
@ -66,6 +111,9 @@ func (s *fileSuggester) browseDir() string {
|
|||
// supports). A failed stat falls through to a fresh ReadDir rather
|
||||
// than returning a stale cache so transient errors self-heal.
|
||||
func (s *fileSuggester) scan() []fileEntry {
|
||||
if s.recursive {
|
||||
return s.scanRecursive()
|
||||
}
|
||||
dir := s.browseDir()
|
||||
var mtime time.Time
|
||||
if info, err := os.Stat(dir); err == nil {
|
||||
|
|
@ -103,6 +151,68 @@ func (s *fileSuggester) scan() []fileEntry {
|
|||
return all
|
||||
}
|
||||
|
||||
// scanRecursive walks the whole project tree below cwd and returns
|
||||
// every file and directory as a fileEntry whose rel is the path
|
||||
// relative to cwd. Common heavy directories (.git, node_modules, ...)
|
||||
// are skipped, and the walk stops once it hits the entry/depth caps.
|
||||
//
|
||||
// Results are cached by cwd + mtime of cwd. Unlike the flat scan a
|
||||
// single mtime can't catch every nested change, so the cache is best
|
||||
// effort; Invalidate() (called on each keystroke path that matters)
|
||||
// and the explicit cache drops on toggle keep it fresh enough for an
|
||||
// interactive picker.
|
||||
func (s *fileSuggester) scanRecursive() []fileEntry {
|
||||
root := s.cwd
|
||||
var mtime time.Time
|
||||
if info, err := os.Stat(root); err == nil {
|
||||
mtime = info.ModTime()
|
||||
}
|
||||
if s.cachedDir == root && s.cachedAll != nil && !mtime.IsZero() && mtime.Equal(s.cachedMTime) {
|
||||
return s.cachedAll
|
||||
}
|
||||
|
||||
var all []fileEntry
|
||||
rootSep := strings.Count(root, string(os.PathSeparator))
|
||||
_ = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
if d != nil && d.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if path == root {
|
||||
return nil
|
||||
}
|
||||
name := d.Name()
|
||||
if d.IsDir() {
|
||||
if recursiveSkipDirs[name] {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if strings.Count(path, string(os.PathSeparator))-rootSep >= maxRecursiveDepth {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
rel, relErr := filepath.Rel(root, path)
|
||||
if relErr != nil {
|
||||
return nil
|
||||
}
|
||||
all = append(all, fileEntry{
|
||||
name: rel,
|
||||
rel: rel,
|
||||
isDir: d.IsDir(),
|
||||
})
|
||||
if len(all) >= maxRecursiveEntries {
|
||||
return filepath.SkipAll
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
s.cachedAll = all
|
||||
s.cachedDir = root
|
||||
s.cachedMTime = mtime
|
||||
return all
|
||||
}
|
||||
|
||||
// extractAtQuery returns the filter string after "@".
|
||||
func extractAtQuery(input string) (string, bool) {
|
||||
input = strings.TrimRight(input, " ")
|
||||
|
|
@ -121,6 +231,11 @@ func extractAtQuery(input string) (string, bool) {
|
|||
}
|
||||
|
||||
// matches returns file entries matching the current @-query.
|
||||
//
|
||||
// An empty query returns every entry in scan order. A non-empty query
|
||||
// is ranked with sahilm/fuzzy: in recursive mode the pattern is matched
|
||||
// against each entry's path relative to cwd (so "foobar" can find
|
||||
// "src/foo/bar.go"); in flat mode it matches the entry's display name.
|
||||
func (s *fileSuggester) matches(input string) []fileEntry {
|
||||
query, ok := extractAtQuery(input)
|
||||
if !ok {
|
||||
|
|
@ -130,14 +245,22 @@ func (s *fileSuggester) matches(input string) []fileEntry {
|
|||
if len(all) == 0 {
|
||||
return nil
|
||||
}
|
||||
needle := strings.ToLower(query)
|
||||
if needle == "" {
|
||||
if query == "" {
|
||||
return all
|
||||
}
|
||||
var out []fileEntry
|
||||
for _, e := range all {
|
||||
if strings.Contains(strings.ToLower(e.name), needle) {
|
||||
out = append(out, e)
|
||||
haystack := make([]string, len(all))
|
||||
for i, e := range all {
|
||||
if s.recursive {
|
||||
haystack[i] = e.rel
|
||||
} else {
|
||||
haystack[i] = e.name
|
||||
}
|
||||
}
|
||||
ranked := fuzzy.Find(query, haystack)
|
||||
out := make([]fileEntry, 0, len(ranked))
|
||||
for _, m := range ranked {
|
||||
if m.Index >= 0 && m.Index < len(all) {
|
||||
out = append(out, all[m.Index])
|
||||
}
|
||||
}
|
||||
return out
|
||||
|
|
@ -176,8 +299,12 @@ func (s *fileSuggester) Down() {
|
|||
}
|
||||
|
||||
// Right opens the selected directory, descending into it.
|
||||
// Returns true if a directory was entered.
|
||||
// Returns true if a directory was entered. Disabled in recursive mode,
|
||||
// where the whole tree is already flattened into the result list.
|
||||
func (s *fileSuggester) Right() bool {
|
||||
if s.recursive {
|
||||
return false
|
||||
}
|
||||
m := s.lastMatches
|
||||
if len(m) == 0 || s.cursor >= len(m) {
|
||||
return false
|
||||
|
|
@ -194,8 +321,11 @@ func (s *fileSuggester) Right() bool {
|
|||
}
|
||||
|
||||
// Left goes back to the parent directory.
|
||||
// Returns true if we moved up.
|
||||
// Returns true if we moved up. Disabled in recursive mode.
|
||||
func (s *fileSuggester) Left() bool {
|
||||
if s.recursive {
|
||||
return false
|
||||
}
|
||||
if s.browseRel == "" {
|
||||
return false
|
||||
}
|
||||
|
|
@ -283,10 +413,13 @@ func (s *fileSuggester) Render(input string, th tui.Theme, width int) []string {
|
|||
}
|
||||
|
||||
out = append(out, "")
|
||||
hint := " \u2191/\u2193 navigate - enter select - esc cancel"
|
||||
if s.browseRel != "" {
|
||||
var hint string
|
||||
switch {
|
||||
case s.recursive:
|
||||
hint = " \u2191/\u2193 navigate - enter select - esc cancel (recursive)"
|
||||
case s.browseRel != "":
|
||||
hint = " \u2191/\u2193 navigate - \u2192 open - \u2190 back - enter select - esc cancel"
|
||||
} else {
|
||||
default:
|
||||
hint = " \u2191/\u2193 navigate - \u2192 open dir - enter select - esc cancel"
|
||||
}
|
||||
out = append(out, th.FG256(th.Muted, hint))
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
|
@ -97,3 +98,98 @@ func (b byDirsFirst) Less(i, j int) bool {
|
|||
}
|
||||
return b[i].name < b[j].name
|
||||
}
|
||||
|
||||
// TestFileSuggesterFuzzyMatch verifies the @-query ranks entries with
|
||||
// a fuzzy subsequence match rather than a plain substring, so a
|
||||
// non-contiguous pattern like "fsg" still finds "file_suggest.go".
|
||||
func TestFileSuggesterFuzzyMatch(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
for _, name := range []string{"file_suggest.go", "interactive.go", "README.md"} {
|
||||
if err := os.WriteFile(filepath.Join(tmp, name), []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
s := newFileSuggester()
|
||||
s.SetCWD(tmp)
|
||||
|
||||
got := s.matches("@fsg")
|
||||
if !containsEntry(got, "file_suggest.go", false) {
|
||||
t.Fatalf("fuzzy query @fsg did not match file_suggest.go: %#v", got)
|
||||
}
|
||||
if len(got) == 0 || got[0].name != "file_suggest.go" {
|
||||
t.Fatalf("file_suggest.go not ranked first for @fsg: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFileSuggesterRecursiveMatch verifies recursive mode flattens the
|
||||
// tree and matches against the cwd-relative path, so a pattern can
|
||||
// span directory boundaries.
|
||||
func TestFileSuggesterRecursiveMatch(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(tmp, "src", "foo"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(tmp, "src", "foo", "bar.go"), []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := newFileSuggester()
|
||||
s.SetCWD(tmp)
|
||||
s.SetRecursive(true)
|
||||
|
||||
rel := filepath.Join("src", "foo", "bar.go")
|
||||
got := s.matches("@foobar")
|
||||
if !containsEntry(got, rel, false) {
|
||||
t.Fatalf("recursive @foobar did not match %s: %#v", rel, got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFileSuggesterRecursiveSkipsHeavyDirs ensures the walk prunes
|
||||
// directories like .git that would otherwise dominate the budget.
|
||||
func TestFileSuggesterRecursiveSkipsHeavyDirs(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(tmp, ".git", "objects"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(tmp, ".git", "objects", "deadbeef"), []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(tmp, "main.go"), []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := newFileSuggester()
|
||||
s.SetCWD(tmp)
|
||||
s.SetRecursive(true)
|
||||
|
||||
all := s.scan()
|
||||
for _, e := range all {
|
||||
if e.rel == ".git" || strings.HasPrefix(e.rel, ".git"+string(filepath.Separator)) {
|
||||
t.Fatalf("recursive scan descended into .git: %#v", e)
|
||||
}
|
||||
}
|
||||
if !containsEntry(all, "main.go", false) {
|
||||
t.Fatalf("recursive scan missing main.go: %#v", all)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFileSuggesterToggleResetsCache verifies SetRecursive drops the
|
||||
// cached scan so the next matches() reflects the new mode.
|
||||
func TestFileSuggesterToggleResetsCache(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(tmp, "pkg"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(tmp, "pkg", "nested.go"), []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := newFileSuggester()
|
||||
s.SetCWD(tmp)
|
||||
|
||||
rel := filepath.Join("pkg", "nested.go")
|
||||
if containsEntry(s.matches("@nested"), rel, false) {
|
||||
t.Fatal("flat mode unexpectedly saw nested.go")
|
||||
}
|
||||
s.SetRecursive(true)
|
||||
if !containsEntry(s.matches("@nested"), rel, false) {
|
||||
t.Fatal("recursive mode did not surface nested.go after toggle")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,11 @@ type InteractiveConfig struct {
|
|||
// re-reading config.json on every open.
|
||||
AutoSwarmEnabled *bool
|
||||
|
||||
// RecursiveFileSuggest mirrors the persisted recursive_file_suggest
|
||||
// flag at startup. When true the @-mention picker fuzzy-searches the
|
||||
// whole project tree instead of browsing one directory at a time.
|
||||
RecursiveFileSuggest *bool
|
||||
|
||||
// ThemeName mirrors the persisted config theme value. Empty means auto.
|
||||
ThemeName string
|
||||
// ExtensionThemes returns themes bundled with loaded extensions.
|
||||
|
|
@ -228,6 +233,7 @@ type chatCacheKey struct {
|
|||
type SettingsStore interface {
|
||||
SetInlineImages(enabled bool) error
|
||||
SetAutoSwarm(enabled bool) error
|
||||
SetRecursiveFileSuggest(enabled bool) error
|
||||
SetReasoning(level string) error
|
||||
SetTheme(name string) error
|
||||
}
|
||||
|
|
@ -474,6 +480,7 @@ func NewInteractive(cfg InteractiveConfig) *Interactive {
|
|||
spin: newSpinner(cfg.Theme),
|
||||
inputHistoryIndex: -1,
|
||||
}
|
||||
i.fileSuggest.SetRecursive(cfg.RecursiveFileSuggest != nil && *cfg.RecursiveFileSuggest)
|
||||
if cfg.Agent != nil {
|
||||
i.agent = cfg.Agent
|
||||
i.view.Messages = cfg.Agent.Messages()
|
||||
|
|
@ -2622,6 +2629,8 @@ func (i *Interactive) openSettingsDialog() {
|
|||
autoSwarmHint = "swarm supervisor not available in this mode"
|
||||
}
|
||||
|
||||
recursiveFiles := i.cfg.RecursiveFileSuggest != nil && *i.cfg.RecursiveFileSuggest
|
||||
|
||||
reasoningOptions := []settingsOption{
|
||||
{value: "", label: "off", desc: "no reasoning"},
|
||||
{value: "minimum", label: "minimum", desc: "very brief (~1k tokens)"},
|
||||
|
|
@ -2685,6 +2694,12 @@ func (i *Interactive) openSettingsDialog() {
|
|||
disabled: autoSwarmDisabled,
|
||||
hint: autoSwarmHint,
|
||||
},
|
||||
{
|
||||
key: "recursive_file_suggest",
|
||||
label: "recursive @-file search",
|
||||
desc: "fuzzy-search the whole project tree when picking files with @ instead of browsing one directory at a time",
|
||||
value: recursiveFiles,
|
||||
},
|
||||
{
|
||||
key: "reasoning",
|
||||
label: "thinking level",
|
||||
|
|
@ -2767,6 +2782,24 @@ func (i *Interactive) applySettingToggle(key string, value bool) {
|
|||
i.statusOK = "auto-swarm " + onOff(value)
|
||||
i.statusErr = ""
|
||||
i.mu.Unlock()
|
||||
case "recursive_file_suggest":
|
||||
val := value
|
||||
i.cfg.RecursiveFileSuggest = &val
|
||||
if i.cfg.SettingsStore != nil {
|
||||
if err := i.cfg.SettingsStore.SetRecursiveFileSuggest(value); err != nil {
|
||||
i.mu.Lock()
|
||||
i.statusErr = "settings: " + err.Error()
|
||||
i.mu.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
// Flip the live picker so the next @ reflects the new mode
|
||||
// without restarting zot. SetRecursive drops its cache.
|
||||
i.fileSuggest.SetRecursive(value)
|
||||
i.mu.Lock()
|
||||
i.statusOK = "recursive @-file search " + onOff(value)
|
||||
i.statusErr = ""
|
||||
i.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,15 @@ func (configSettingsStore) SetAutoSwarm(enabled bool) error {
|
|||
return SaveConfig(cfg)
|
||||
}
|
||||
|
||||
func (configSettingsStore) SetRecursiveFileSuggest(enabled bool) error {
|
||||
cfg, err := LoadConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.RecursiveFileSuggest = &enabled
|
||||
return SaveConfig(cfg)
|
||||
}
|
||||
|
||||
func (configSettingsStore) SetReasoning(level string) error {
|
||||
cfg, err := LoadConfig()
|
||||
if err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue