feat: Go to scopes are grouped like the Sources pane, an account at a time
CI / Checks (pull_request) Successful in 24s
PR title / Conventional title (pull_request) Successful in 1s

The scope list follows the Sources pane: All, then each account (or
Databricks, or Repos) with its sources under it, in place of All Azure,
All Databricks and All repos. An account scope searches and loads only
that account. ctrl+s on a group header narrows to its group.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kebandClaude Opus 5.5 committed 2026-10-12 11:51:29 +02:00
1 parent 57aec8a381
commit 331e53fb49
7 files changed
+124 -74

No files matched your search

+63 -51
View File
@@ -68,10 +68,10 @@ const (
var tabNames = [tabCount]string{"Search", "Favourites", "Recent"}
const (
scopeFavs = "#favs"
scopeRecent = "#recent"
allSources = "#all"
kindScope = "#kind:" // + a provider kind: all of Azure, Databricks or repos
scopeFavs = "#favs"
scopeRecent = "#recent"
allSources = "#all"
accountScope = "#account:" // + a provider ID: an account's sources, or Databricks', or the repos
)
const goToTitle = "Go to"
@@ -138,7 +138,11 @@ func (v *searchView) onSource() bool { return v.tab == tabSearch && v.scope().gr
func (m *Model) preselect() {
v := m.search
row := m.sources.SelectedID()
i := slices.IndexFunc(v.scopes, func(o option) bool { return o.group != "" && o.group == row })
account := ""
if id, ok := strings.CutPrefix(row, "group:"); ok { // a group header
account = accountScope + id
}
i := slices.IndexFunc(v.scopes, func(o option) bool { return o.id == account || o.group != "" && o.group == row })
if i < 0 {
return
}
@@ -172,25 +176,36 @@ func (m *Model) fillSearch(keep bool) {
}
sources, items := m.searchEntries()
all := option{label: "All", id: allSources, mark: m.coverageMark("")}
scopes := []option{all}
// All of one kind, so a search can stay on repos (local, quick)
// without asking Azure for every vault; only with several kinds.
var kinds []provider.Kind
all := option{label: "All", id: allSources, mark: m.coverageMark(nil)}
// Grouped like the Sources pane: each account (or Databricks, or the
// repos) is a scope too, its sources under it, so a search can stay on
// one, like the repos (local, quick), without asking Azure for every
// vault. With only one, it's the same as All.
var groups []provider.Provider
for _, p := range m.providers {
if len(m.sourceResults[p.ID()].sources) > 0 && !slices.Contains(kinds, p.Kind()) {
kinds = append(kinds, p.Kind())
if len(m.sourceResults[p.ID()].sources) > 0 {
groups = append(groups, p)
}
}
if len(kinds) > 1 {
for _, k := range kinds {
scopes := []option{all}
if len(groups) < 2 {
scopes = append(scopes, sources...)
} else {
for _, p := range groups {
k := p.Kind()
scopes = append(scopes, option{
label: m.st.Kind(k).Render(m.icons.Prefix(k) + kindScopeLabel(k)),
id: kindScope + string(k), kind: k, mark: m.coverageMark(k),
label: m.st.Kind(k).Render(m.icons.Prefix(k) + p.Label()),
id: accountScope + p.ID(), kind: k, mark: m.coverageMark(ofProvider(p.ID())),
})
for _, o := range sources {
if providerOf(o.group) == p.ID() {
o.label = " " + o.label
scopes = append(scopes, o)
}
}
}
}
v.scopes, v.items = append(scopes, sources...), items
v.scopes, v.items = scopes, items
v.src = max(0, slices.IndexFunc(v.scopes, func(o option) bool { return o.id == atSrc }))
v.shown = v.match()
@@ -203,17 +218,9 @@ func (m *Model) fillSearch(keep bool) {
v.res = max(0, min(v.res, len(v.shown)-1))
}
// kindScopeLabel names the scope of all of a kind's sources.
func kindScopeLabel(k provider.Kind) string {
if k == provider.KindRepo {
return "All repos"
}
return "All " + string(k)
}
// coverageMark is ○ ◐ ● for how much of a kind ("" for all) is loaded.
func (m *Model) coverageMark(kind provider.Kind) string {
switch c := m.coverageOf(kind); {
// coverageMark is ○ ◐ ● for how much of the providers in is loaded.
func (m *Model) coverageMark(in providerFilter) string {
switch c := m.coverageOf(in); {
case c.complete():
return m.st.Success.Render("●")
case c.vaultsLoaded > 0:
@@ -222,15 +229,28 @@ func (m *Model) coverageMark(kind provider.Kind) string {
return m.st.Muted.Render("○")
}
// scopeKind is the kind a scope covers all of, if it's one of those.
func (v *searchView) scopeKind() (provider.Kind, bool) {
k, ok := strings.CutPrefix(v.scope().id, kindScope)
// scopeAccount is the provider whose sources the scope covers, if it's
// an account's.
func (v *searchView) scopeAccount() (string, bool) {
id, ok := strings.CutPrefix(v.scope().id, accountScope)
if !ok || v.tab != tabSearch {
return "", false
}
return provider.Kind(k), true
return id, true
}
// scopeFilter is what loading everything in the scope covers: an
// account's, else all of them.
func (v *searchView) scopeFilter() providerFilter {
if id, ok := v.scopeAccount(); ok {
return ofProvider(id)
}
return nil
}
// scopeName is the scope's label without colours or indent.
func scopeName(o option) string { return strings.TrimSpace(ansi.Strip(o.label)) }
// match lists the scope's entries matching the input, best first.
func (v *searchView) match() []option {
var cand []option
@@ -240,9 +260,9 @@ func (v *searchView) match() []option {
case scopeRecent:
cand = v.recent
default:
kind, ofKind := v.scopeKind()
account, ofAccount := v.scopeAccount()
for _, o := range v.items {
if sc.id == allSources || ofKind && o.kind == kind || sc.group != "" && o.group == sc.group {
if sc.id == allSources || ofAccount && providerOf(o.group) == account || sc.group != "" && o.group == sc.group {
cand = append(cand, o)
}
}
@@ -421,11 +441,9 @@ func (m *Model) handleSearch(msg tea.KeyPressMsg) tea.Cmd {
if v.onSource() {
return v.scope().reload(m)
}
kind, _ := v.scopeKind()
return m.startCrawlOf(kind)
return m.startCrawlOf(v.scopeFilter())
case inSearch && key.Matches(msg, k.loadAll):
kind, _ := v.scopeKind()
return m.startCrawlOf(kind)
return m.startCrawlOf(v.scopeFilter())
case key.Matches(msg, k.star):
if o, ok := v.selected(); ok && o.star != "" {
m.toggleStarKey(o.star, o.starAccount)
@@ -488,8 +506,8 @@ func (m *Model) searchLoads() tea.Cmd {
v.loaded = map[string]bool{}
}
v.loaded[sc.id] = true
if kind, ok := v.scopeKind(); ok {
return m.startCrawlOf(kind)
if _, ok := v.scopeAccount(); ok {
return m.startCrawlOf(v.scopeFilter())
}
if !v.onSource() {
return m.startCrawl()
@@ -557,7 +575,7 @@ func (m Model) narrowHint() string {
if i < 0 || v.scope().id != allSources {
return ""
}
return withKey(m.keys.Search, "%s only "+ansi.Strip(v.scopes[i].label))
return withKey(m.keys.Search, "%s only "+scopeName(v.scopes[i]))
}
// loadHint says how much of the scope is loaded, and the key to load it.
@@ -569,8 +587,8 @@ func (m Model) loadHint() string {
if v.tab != tabSearch {
return "" // from the store: nothing to load
}
if kind, ok := v.scopeKind(); ok || v.scope().id == allSources {
c := m.coverageOf(kind)
if _, ok := v.scopeAccount(); ok || v.scope().id == allSources {
c := m.coverageOf(v.scopeFilter())
if c.complete() {
return ""
}
@@ -702,14 +720,8 @@ func (m Model) searchViewBox() string {
}
title := func(s string, active bool) string { return panes.TitleStyle(m.st, active && focused).Render(s) }
resultsTitle := map[string]string{scopeFavs: "Favourites", scopeRecent: "Recent", allSources: "Results in all sources"}[v.scope().id]
if v.onSource() {
resultsTitle = "Results in " + ansi.Strip(v.scope().label)
}
if kind, ok := v.scopeKind(); ok {
resultsTitle = "Results in " + string(kind)
if kind == provider.KindRepo {
resultsTitle = "Results in all repos"
}
if _, ok := v.scopeAccount(); ok || v.onSource() {
resultsTitle = "Results in " + scopeName(v.scope())
}
var lists string
if v.tab == tabSearch {