Просмотр исходного кода

feat(search): Add fuzzy name matching and mixed sources (#1719)

* fix(alpm): fix callback text

* feat(yay): Add mixed search result

* remove old result structs

* add option for controlling query builder

* only set query builder after parsing args

* add parser args

* update manpage

* write test for results

* write test for results

* mixed source test

* only sort 1 time with every mode
J Guerreiro 3 лет назад
Родитель
Сommit
e4a1f018ea

+ 7 - 0
.golangci.yml

@@ -68,6 +68,13 @@ linters:
     - whitespace
     - wsl
     - godot
+    # - maligned
+    # - interfacer
+    # - nilerr
+    # - nlreturn
+    # - exhaustivestruct
+    # - errname
+    # - forbidigo
 
 run:
 

+ 7 - 11
cmd.go

@@ -185,7 +185,7 @@ func handleCmd(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.Exe
 	case "P", "show":
 		return handlePrint(ctx, cmdArgs, dbExecutor)
 	case "Y", "--yay":
-		return handleYay(ctx, cmdArgs, dbExecutor)
+		return handleYay(ctx, cmdArgs, dbExecutor, config.Runtime.QueryBuilder)
 	}
 
 	return errors.New(gotext.Get("unhandled operation"))
@@ -289,7 +289,7 @@ func handlePrint(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.E
 	return nil
 }
 
-func handleYay(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.Executor) error {
+func handleYay(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.Executor, queryBuilder query.Builder) error {
 	switch {
 	case cmdArgs.ExistsArg("gendb"):
 		return createDevelDB(ctx, config, dbExecutor)
@@ -298,7 +298,7 @@ func handleYay(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.Exe
 	case cmdArgs.ExistsArg("c", "clean"):
 		return cleanDependencies(ctx, cmdArgs, dbExecutor, false)
 	case len(cmdArgs.Targets) > 0:
-		return handleYogurt(ctx, cmdArgs, dbExecutor)
+		return displayNumberMenu(ctx, cmdArgs.Targets, dbExecutor, queryBuilder, cmdArgs)
 	}
 
 	return nil
@@ -312,16 +312,12 @@ func handleGetpkgbuild(ctx context.Context, cmdArgs *parser.Arguments, dbExecuto
 	return getPkgbuilds(ctx, dbExecutor, config, cmdArgs.Targets, cmdArgs.ExistsArg("f", "force"))
 }
 
-func handleYogurt(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.Executor) error {
-	return displayNumberMenu(ctx, cmdArgs.Targets, dbExecutor, cmdArgs)
-}
-
 func handleSync(ctx context.Context, cmdArgs *parser.Arguments, dbExecutor db.Executor) error {
 	targets := cmdArgs.Targets
 
 	switch {
 	case cmdArgs.ExistsArg("s", "search"):
-		return syncSearch(ctx, targets, config.Runtime.AURClient, dbExecutor, !cmdArgs.ExistsArg("q", "quiet"))
+		return syncSearch(ctx, targets, config.Runtime.AURClient, dbExecutor, config.Runtime.QueryBuilder, !cmdArgs.ExistsArg("q", "quiet"))
 	case cmdArgs.ExistsArg("p", "print", "print-format"):
 		return config.Runtime.CmdBuilder.Show(config.Runtime.CmdBuilder.BuildPacmanCmd(ctx,
 			cmdArgs, config.Runtime.Mode, settings.NoConfirm))
@@ -355,9 +351,9 @@ func handleRemove(ctx context.Context, cmdArgs *parser.Arguments, localCache *vc
 }
 
 // NumberMenu presents a CLI for selecting packages to install.
-func displayNumberMenu(ctx context.Context, pkgS []string, dbExecutor db.Executor, cmdArgs *parser.Arguments) error {
-	queryBuilder := query.NewSourceQueryBuilder(config.SortBy, config.Runtime.Mode, config.SearchBy, config.BottomUp, config.SingleLineResults)
-
+func displayNumberMenu(ctx context.Context, pkgS []string, dbExecutor db.Executor,
+	queryBuilder query.Builder, cmdArgs *parser.Arguments,
+) error {
 	queryBuilder.Execute(ctx, dbExecutor, config.Runtime.AURClient, pkgS)
 
 	if err := queryBuilder.Results(os.Stdout, dbExecutor, query.NumberMenu); err != nil {

+ 8 - 0
doc/yay.8

@@ -383,6 +383,14 @@ the last modification time of each package's AUR page.
 Do not consider build times during sysupgrade.
 
 .TP
+.B \-\-separatesources
+Separate query results by source, AUR and sync
+
+.TP
+.B \-\-noseparatesources
+Do not separate query results by source for searching
+
+.TP
 .B \-\-redownload
 Always download pkgbuilds of targets even when a copy is available in cache.
 

+ 1 - 0
go.mod

@@ -15,6 +15,7 @@ require (
 )
 
 require (
+	github.com/adrg/strutil v0.2.3
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect

+ 2 - 0
go.sum

@@ -6,6 +6,8 @@ github.com/Morganamilo/go-pacmanconf v0.0.0-20210502114700-cff030e927a5 h1:TMscP
 github.com/Morganamilo/go-pacmanconf v0.0.0-20210502114700-cff030e927a5/go.mod h1:Hk55m330jNiwxRodIlMCvw5iEyoRUCIY64W1p9D+tHc=
 github.com/Morganamilo/go-srcinfo v1.0.0 h1:Wh4nEF+HJWo+29hnxM18Q2hi+DUf0GejS13+Wg+dzmI=
 github.com/Morganamilo/go-srcinfo v1.0.0/go.mod h1:MP6VGY1NNpVUmYIEgoM9acix95KQqIRyqQ0hCLsyYUY=
+github.com/adrg/strutil v0.2.3 h1:WZVn3ItPBovFmP4wMHHVXUr8luRaHrbyIuLlHt32GZQ=
+github.com/adrg/strutil v0.2.3/go.mod h1:+SNxbiH6t+O+5SZqIj5n/9i5yUjR+S3XXVrjEcN2mxg=
 github.com/bradleyjkemp/cupaloy v2.3.0+incompatible h1:UafIjBvWQmS9i/xRg+CamMrnLTKNzo+bdmT/oH34c2Y=
 github.com/bradleyjkemp/cupaloy v2.3.0+incompatible/go.mod h1:Au1Xw1sgaJ5iSFktEhYsS0dbQiS1B0/XMXl+42y9Ilk=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

+ 9 - 0
main.go

@@ -12,6 +12,7 @@ import (
 
 	"github.com/Jguer/yay/v11/pkg/db"
 	"github.com/Jguer/yay/v11/pkg/db/ialpm"
+	"github.com/Jguer/yay/v11/pkg/query"
 	"github.com/Jguer/yay/v11/pkg/settings"
 	"github.com/Jguer/yay/v11/pkg/settings/parser"
 	"github.com/Jguer/yay/v11/pkg/text"
@@ -131,6 +132,14 @@ func main() {
 		}
 	}
 
+	if config.SeparateSources {
+		config.Runtime.QueryBuilder = query.NewSourceQueryBuilder(config.SortBy,
+			config.Runtime.Mode, config.SearchBy, config.BottomUp, config.SingleLineResults)
+	} else {
+		config.Runtime.QueryBuilder = query.NewMixedSourceQueryBuilder(config.SortBy,
+			config.Runtime.Mode, config.SearchBy, config.BottomUp, config.SingleLineResults)
+	}
+
 	var useColor bool
 
 	config.Runtime.PacmanConf, useColor, err = initAlpm(cmdArgs, config.PacmanConf)

+ 3 - 2
pkg/db/ialpm/alpm.go

@@ -160,7 +160,7 @@ func (ae *AlpmExecutor) questionCallback() func(question alpm.QuestionAny) {
 			return nil
 		})
 
-		str := text.Bold(gotext.Get("There are %d providers available for %s:\n", size, qp.Dep()))
+		str := text.Bold(gotext.Get("There are %d providers available for %s:", size, qp.Dep()))
 
 		size = 1
 
@@ -171,7 +171,8 @@ func (ae *AlpmExecutor) questionCallback() func(question alpm.QuestionAny) {
 
 			if dbName != thisDB {
 				dbName = thisDB
-				str += text.SprintOperationInfo(gotext.Get("Repository"), dbName, "\n    ")
+				str += "\n"
+				str += text.SprintOperationInfo(gotext.Get("Repository"), " ", dbName, "\n    ")
 			}
 			str += fmt.Sprintf("%d) %s ", size, pkg.Name())
 			size++

+ 1 - 1
pkg/db/mock/repo.go

@@ -148,7 +148,7 @@ func (p *Package) Packager() string {
 
 // Provides returns DependList of packages provides by package.
 func (p *Package) Provides() alpm.DependList {
-	panic("not implemented") // TODO: Implement
+	return alpm.DependList{}
 }
 
 // Origin returns package origin.

+ 262 - 0
pkg/query/mixed_sources.go

@@ -0,0 +1,262 @@
+package query
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"sort"
+	"strconv"
+	"strings"
+
+	"github.com/Jguer/aur"
+	"github.com/Jguer/go-alpm/v2"
+	"github.com/adrg/strutil"
+	"github.com/adrg/strutil/metrics"
+	"github.com/leonelquinteros/gotext"
+
+	"github.com/Jguer/yay/v11/pkg/db"
+	"github.com/Jguer/yay/v11/pkg/intrange"
+	"github.com/Jguer/yay/v11/pkg/settings/parser"
+	"github.com/Jguer/yay/v11/pkg/stringset"
+	"github.com/Jguer/yay/v11/pkg/text"
+)
+
+const sourceAUR = "aur"
+
+type Builder interface {
+	Len() int
+	Execute(ctx context.Context, dbExecutor db.Executor, aurClient *aur.Client, pkgS []string)
+	Results(w io.Writer, dbExecutor db.Executor, verboseSearch SearchVerbosity) error
+	GetTargets(include, exclude intrange.IntRanges, otherExclude stringset.StringSet) ([]string, error)
+}
+
+type MixedSourceQueryBuilder struct {
+	results           []abstractResult
+	sortBy            string
+	searchBy          string
+	targetMode        parser.TargetMode
+	queryMap          map[string]map[string]interface{}
+	bottomUp          bool
+	singleLineResults bool
+}
+
+func NewMixedSourceQueryBuilder(
+	sortBy string,
+	targetMode parser.TargetMode,
+	searchBy string,
+	bottomUp,
+	singleLineResults bool,
+) *MixedSourceQueryBuilder {
+	return &MixedSourceQueryBuilder{
+		bottomUp:          bottomUp,
+		sortBy:            sortBy,
+		targetMode:        targetMode,
+		searchBy:          searchBy,
+		singleLineResults: singleLineResults,
+		queryMap:          map[string]map[string]interface{}{},
+		results:           make([]abstractResult, 0, 100),
+	}
+}
+
+type abstractResult struct {
+	source      string
+	name        string
+	description string
+	votes       int
+	provides    []string
+}
+
+type abstractResults struct {
+	results       []abstractResult
+	search        string
+	distanceCache map[string]float64
+	bottomUp      bool
+	metric        strutil.StringMetric
+}
+
+func (a *abstractResults) Len() int      { return len(a.results) }
+func (a *abstractResults) Swap(i, j int) { a.results[i], a.results[j] = a.results[j], a.results[i] }
+
+func (a *abstractResults) GetMetric(pkg *abstractResult) float64 {
+	if v, ok := a.distanceCache[pkg.name]; ok {
+		return v
+	}
+
+	sim := strutil.Similarity(pkg.name, a.search, a.metric)
+
+	for _, prov := range pkg.provides {
+		// If the package provides search, it's a perfect match
+		// AUR packages don't populate provides
+		candidate := strutil.Similarity(prov, a.search, a.metric)
+		if candidate > sim {
+			sim = candidate
+		}
+	}
+
+	simDesc := strutil.Similarity(pkg.description, a.search, a.metric)
+
+	// slightly overweight sync sources by always giving them max popularity
+	popularity := 1.0
+	if pkg.source == sourceAUR {
+		popularity = float64(pkg.votes) / float64(pkg.votes+60)
+	}
+
+	sim = sim*0.6 + simDesc*0.2 + popularity*0.2
+
+	a.distanceCache[pkg.name] = sim
+
+	return sim
+}
+
+func (a *abstractResults) Less(i, j int) bool {
+	pkgA := a.results[i]
+	pkgB := a.results[j]
+
+	simA := a.GetMetric(&pkgA)
+	simB := a.GetMetric(&pkgB)
+
+	if a.bottomUp {
+		return simA < simB
+	}
+
+	return simA > simB
+}
+
+func (s *MixedSourceQueryBuilder) Execute(ctx context.Context, dbExecutor db.Executor, aurClient *aur.Client, pkgS []string) {
+	var aurErr error
+
+	pkgS = RemoveInvalidTargets(pkgS, s.targetMode)
+
+	metric := &metrics.JaroWinkler{
+		CaseSensitive: false,
+	}
+
+	sortableResults := &abstractResults{
+		results:       []abstractResult{},
+		search:        strings.Join(pkgS, ""),
+		distanceCache: map[string]float64{},
+		bottomUp:      s.bottomUp,
+		metric:        metric,
+	}
+
+	if s.targetMode.AtLeastAUR() {
+		var aurResults aurQuery
+		aurResults, aurErr = queryAUR(ctx, aurClient, pkgS, s.searchBy)
+		dbName := sourceAUR
+
+		for i := range aurResults {
+			if s.queryMap[dbName] == nil {
+				s.queryMap[dbName] = map[string]interface{}{}
+			}
+
+			s.queryMap[dbName][aurResults[i].Name] = aurResults[i]
+
+			sortableResults.results = append(sortableResults.results, abstractResult{
+				source:      dbName,
+				name:        aurResults[i].Name,
+				description: aurResults[i].Description,
+				provides:    aurResults[i].Provides,
+				votes:       aurResults[i].NumVotes,
+			})
+		}
+	}
+
+	var repoResults []alpm.IPackage
+	if s.targetMode.AtLeastRepo() {
+		repoResults = dbExecutor.SyncPackages(pkgS...)
+
+		for i := range repoResults {
+			dbName := repoResults[i].DB().Name()
+			if s.queryMap[dbName] == nil {
+				s.queryMap[dbName] = map[string]interface{}{}
+			}
+
+			s.queryMap[dbName][repoResults[i].Name()] = repoResults[i]
+
+			rawProvides := repoResults[i].Provides().Slice()
+
+			provides := make([]string, len(rawProvides))
+			for j := range rawProvides {
+				provides[j] = rawProvides[j].Name
+			}
+
+			sortableResults.results = append(sortableResults.results, abstractResult{
+				source:      repoResults[i].DB().Name(),
+				name:        repoResults[i].Name(),
+				description: repoResults[i].Description(),
+				provides:    provides,
+				votes:       -1,
+			})
+		}
+	}
+
+	sort.Sort(sortableResults)
+	s.results = sortableResults.results
+
+	if aurErr != nil {
+		text.Errorln(ErrAURSearch{inner: aurErr})
+
+		if len(repoResults) != 0 {
+			text.Warnln(gotext.Get("Showing repo packages only"))
+		}
+	}
+}
+
+func (s *MixedSourceQueryBuilder) Results(w io.Writer, dbExecutor db.Executor, verboseSearch SearchVerbosity) error {
+	for i := range s.results {
+		if verboseSearch == Minimal {
+			_, _ = fmt.Fprintln(w, s.results[i].name)
+			continue
+		}
+
+		var toPrint string
+
+		if verboseSearch == NumberMenu {
+			if s.bottomUp {
+				toPrint += text.Magenta(strconv.Itoa(len(s.results)-i)) + " "
+			} else {
+				toPrint += text.Magenta(strconv.Itoa(i+1)) + " "
+			}
+		}
+
+		pkg := s.queryMap[s.results[i].source][s.results[i].name]
+		if s.results[i].source == sourceAUR {
+			aurPkg := pkg.(aur.Pkg)
+			toPrint += aurPkgSearchString(&aurPkg, dbExecutor, s.singleLineResults)
+		} else {
+			syncPkg := pkg.(alpm.IPackage)
+			toPrint += syncPkgSearchString(syncPkg, dbExecutor, s.singleLineResults)
+		}
+
+		fmt.Fprintln(w, toPrint)
+	}
+
+	return nil
+}
+
+func (s *MixedSourceQueryBuilder) Len() int {
+	return len(s.results)
+}
+
+func (s *MixedSourceQueryBuilder) GetTargets(include, exclude intrange.IntRanges,
+	otherExclude stringset.StringSet,
+) ([]string, error) {
+	var (
+		isInclude = len(exclude) == 0 && len(otherExclude) == 0
+		targets   []string
+		lenRes    = len(s.results)
+	)
+
+	for i := 0; i <= s.Len(); i++ {
+		target := i - 1
+		if s.bottomUp {
+			target = lenRes - i
+		}
+
+		if (isInclude && include.Get(i)) || (!isInclude && !exclude.Get(i)) {
+			targets = append(targets, s.results[target].source+"/"+s.results[target].name)
+		}
+	}
+
+	return targets, nil
+}

Разница между файлами не показана из-за своего большого размера
+ 61 - 0
pkg/query/mixed_sources_test.go


+ 42 - 71
pkg/query/source.go

@@ -29,10 +29,10 @@ const (
 type SourceQueryBuilder struct {
 	repoQuery
 	aurQuery
-	bottomUp          bool
 	sortBy            string
-	targetMode        parser.TargetMode
 	searchBy          string
+	targetMode        parser.TargetMode
+	bottomUp          bool
 	singleLineResults bool
 }
 
@@ -60,11 +60,18 @@ func (s *SourceQueryBuilder) Execute(ctx context.Context, dbExecutor db.Executor
 	pkgS = RemoveInvalidTargets(pkgS, s.targetMode)
 
 	if s.targetMode.AtLeastAUR() {
-		s.aurQuery, aurErr = queryAUR(ctx, aurClient, pkgS, s.searchBy, s.bottomUp, s.sortBy)
+		s.aurQuery, aurErr = queryAUR(ctx, aurClient, pkgS, s.searchBy)
+		s.aurQuery = filterAURResults(pkgS, s.aurQuery)
+
+		sort.Sort(aurSortable{aurQuery: s.aurQuery, sortBy: s.sortBy, bottomUp: s.bottomUp})
 	}
 
 	if s.targetMode.AtLeastRepo() {
-		s.repoQuery = queryRepo(pkgS, dbExecutor, s.bottomUp)
+		s.repoQuery = repoQuery(dbExecutor.SyncPackages(pkgS...))
+
+		if s.bottomUp {
+			s.Reverse()
+		}
 	}
 
 	if aurErr != nil && len(s.repoQuery) != 0 {
@@ -104,7 +111,8 @@ func (s *SourceQueryBuilder) Len() int {
 }
 
 func (s *SourceQueryBuilder) GetTargets(include, exclude intrange.IntRanges,
-	otherExclude stringset.StringSet) ([]string, error) {
+	otherExclude stringset.StringSet,
+) ([]string, error) {
 	isInclude := len(exclude) == 0 && len(otherExclude) == 0
 
 	var targets []string
@@ -140,85 +148,48 @@ func (s *SourceQueryBuilder) GetTargets(include, exclude intrange.IntRanges,
 	return targets, nil
 }
 
-// queryRepo handles repo searches. Creates a RepoSearch struct.
-func queryRepo(pkgInputN []string, dbExecutor db.Executor, bottomUp bool) repoQuery {
-	s := repoQuery(dbExecutor.SyncPackages(pkgInputN...))
+// filter AUR results to remove strings that don't contain all of the search terms.
+func filterAURResults(pkgS []string, results []aur.Pkg) []aur.Pkg {
+	aurPkgs := make([]aur.Pkg, 0, len(results))
+
+	matchesSearchTerms := func(pkg *aur.Pkg, terms []string) bool {
+		for _, pkgN := range terms {
+			name := strings.ToLower(pkg.Name)
+			desc := strings.ToLower(pkg.Description)
+			targ := strings.ToLower(pkgN)
+
+			if !(strings.Contains(name, targ) || strings.Contains(desc, targ)) {
+				return false
+			}
+		}
+
+		return true
+	}
 
-	if bottomUp {
-		s.Reverse()
+	for i := range results {
+		if matchesSearchTerms(&results[i], pkgS) {
+			aurPkgs = append(aurPkgs, results[i])
+		}
 	}
 
-	return s
+	return aurPkgs
 }
 
 // queryAUR searches AUR and narrows based on subarguments.
-func queryAUR(ctx context.Context, aurClient *aur.Client, pkgS []string, searchBy string, bottomUp bool, sortBy string) (aurQuery, error) {
+func queryAUR(ctx context.Context, aurClient *aur.Client, pkgS []string, searchBy string) ([]aur.Pkg, error) {
 	var (
-		r         []aur.Pkg
-		err       error
-		usedIndex int
+		err error
+		by  = getSearchBy(searchBy)
 	)
 
-	by := getSearchBy(searchBy)
+	for _, word := range pkgS {
+		var r []aur.Pkg
 
-	if len(pkgS) == 0 {
-		return nil, nil
-	}
-
-	for i, word := range pkgS {
 		r, err = aurClient.Search(ctx, word, by)
 		if err == nil {
-			usedIndex = i
-
-			break
+			return r, nil
 		}
 	}
 
-	if err != nil {
-		return nil, err
-	}
-
-	if len(pkgS) == 1 {
-		sort.Sort(aurSortable{
-			aurQuery: r,
-			sortBy:   sortBy,
-			bottomUp: bottomUp,
-		})
-
-		return r, err
-	}
-
-	aq := make(aurQuery, 0, len(r))
-
-	for i := range r {
-		match := true
-
-		for j, pkgN := range pkgS {
-			if usedIndex == j {
-				continue
-			}
-
-			name := strings.ToLower(r[i].Name)
-			desc := strings.ToLower(r[i].Description)
-			targ := strings.ToLower(pkgN)
-
-			if !(strings.Contains(name, targ) || strings.Contains(desc, targ)) {
-				match = false
-
-				break
-			}
-		}
-
-		if match {
-			aq = append(aq, r[i])
-		}
-	}
-
-	sort.Sort(aurSortable{
-		aurQuery: aq,
-		sortBy:   sortBy,
-		bottomUp: bottomUp,
-	})
-
-	return aq, err
+	return nil, err
 }

Разница между файлами не показана из-за своего большого размера
+ 136 - 0
pkg/query/source_test.go


+ 61 - 45
pkg/query/types.go

@@ -115,36 +115,46 @@ func (q aurQuery) printSearch(
 			}
 		}
 
-		toprint += text.Bold(text.ColorHash("aur")) + "/" + text.Bold(q[i].Name) +
-			" " + text.Cyan(q[i].Version) +
-			text.Bold(" (+"+strconv.Itoa(q[i].NumVotes)) +
-			" " + text.Bold(strconv.FormatFloat(q[i].Popularity, 'f', 2, 64)+") ")
-
-		if q[i].Maintainer == "" {
-			toprint += text.Bold(text.Red(gotext.Get("(Orphaned)"))) + " "
-		}
+		toprint += aurPkgSearchString(&q[i], dbExecutor, singleLineResults)
+		_, _ = fmt.Fprintln(w, toprint)
+	}
+}
 
-		if q[i].OutOfDate != 0 {
-			toprint += text.Bold(text.Red(gotext.Get("(Out-of-date: %s)", text.FormatTime(q[i].OutOfDate)))) + " "
-		}
+func aurPkgSearchString(
+	pkg *aur.Pkg,
+	dbExecutor db.Executor,
+	singleLineResults bool,
+) string {
+	toPrint := text.Bold(text.ColorHash("aur")) + "/" + text.Bold(pkg.Name) +
+		" " + text.Cyan(pkg.Version) +
+		text.Bold(" (+"+strconv.Itoa(pkg.NumVotes)) +
+		" " + text.Bold(strconv.FormatFloat(pkg.Popularity, 'f', 2, 64)+") ")
+
+	if pkg.Maintainer == "" {
+		toPrint += text.Bold(text.Red(gotext.Get("(Orphaned)"))) + " "
+	}
 
-		if pkg := dbExecutor.LocalPackage(q[i].Name); pkg != nil {
-			if pkg.Version() != q[i].Version {
-				toprint += text.Bold(text.Green(gotext.Get("(Installed: %s)", pkg.Version())))
-			} else {
-				toprint += text.Bold(text.Green(gotext.Get("(Installed)")))
-			}
-		}
+	if pkg.OutOfDate != 0 {
+		toPrint += text.Bold(text.Red(gotext.Get("(Out-of-date: %s)", text.FormatTime(pkg.OutOfDate)))) + " "
+	}
 
-		if singleLineResults {
-			toprint += "\t"
+	if localPkg := dbExecutor.LocalPackage(pkg.Name); localPkg != nil {
+		if localPkg.Version() != pkg.Version {
+			toPrint += text.Bold(text.Green(gotext.Get("(Installed: %s)", localPkg.Version())))
 		} else {
-			toprint += "\n    "
+			toPrint += text.Bold(text.Green(gotext.Get("(Installed)")))
 		}
+	}
 
-		toprint += q[i].Description
-		_, _ = fmt.Fprintln(w, toprint)
+	if singleLineResults {
+		toPrint += "\t"
+	} else {
+		toPrint += "\n    "
 	}
+
+	toPrint += pkg.Description
+
+	return toPrint
 }
 
 // PrintSearch receives a RepoSearch type and outputs pretty text.
@@ -165,32 +175,38 @@ func (r repoQuery) printSearch(w io.Writer, dbExecutor db.Executor, searchMode S
 			}
 		}
 
-		toprint += text.Bold(text.ColorHash(res.DB().Name())) + "/" + text.Bold(res.Name()) +
-			" " + text.Cyan(res.Version()) +
-			text.Bold(" ("+text.Human(res.Size())+
-				" "+text.Human(res.ISize())+") ")
-
-		packageGroups := dbExecutor.PackageGroups(res)
-		if len(packageGroups) != 0 {
-			toprint += fmt.Sprint(packageGroups, " ")
-		}
+		toprint += syncPkgSearchString(res, dbExecutor, singleLineResults)
+		_, _ = fmt.Fprintln(w, toprint)
+	}
+}
 
-		if pkg := dbExecutor.LocalPackage(res.Name()); pkg != nil {
-			if pkg.Version() != res.Version() {
-				toprint += text.Bold(text.Green(gotext.Get("(Installed: %s)", pkg.Version())))
-			} else {
-				toprint += text.Bold(text.Green(gotext.Get("(Installed)")))
-			}
-		}
+// PrintSearch receives a RepoSearch type and outputs pretty text.
+func syncPkgSearchString(pkg alpm.IPackage, dbExecutor db.Executor, singleLineResults bool) string {
+	toPrint := text.Bold(text.ColorHash(pkg.DB().Name())) + "/" + text.Bold(pkg.Name()) +
+		" " + text.Cyan(pkg.Version()) +
+		text.Bold(" ("+text.Human(pkg.Size())+
+			" "+text.Human(pkg.ISize())+") ")
+
+	packageGroups := dbExecutor.PackageGroups(pkg)
+	if len(packageGroups) != 0 {
+		toPrint += fmt.Sprint(packageGroups, " ")
+	}
 
-		if singleLineResults {
-			toprint += "\t"
+	if localPkg := dbExecutor.LocalPackage(pkg.Name()); localPkg != nil {
+		if localPkg.Version() != pkg.Version() {
+			toPrint += text.Bold(text.Green(gotext.Get("(Installed: %s)", localPkg.Version())))
 		} else {
-			toprint += "\n    "
+			toPrint += text.Bold(text.Green(gotext.Get("(Installed)")))
 		}
+	}
 
-		toprint += res.Description()
-
-		_, _ = fmt.Fprintln(w, toprint)
+	if singleLineResults {
+		toPrint += "\t"
+	} else {
+		toPrint += "\n    "
 	}
+
+	toPrint += pkg.Description()
+
+	return toPrint
 }

+ 4 - 0
pkg/settings/args.go

@@ -183,6 +183,10 @@ func (c *Configuration) handleOption(option, value string) bool {
 		c.RemoveMake = "no"
 	case "askremovemake":
 		c.RemoveMake = "ask"
+	case "separatesources":
+		c.SeparateSources = true
+	case "noseparatesources":
+		c.SeparateSources = false
 	default:
 		return false
 	}

+ 2 - 0
pkg/settings/config.go

@@ -70,6 +70,7 @@ type Configuration struct {
 	UseAsk             bool     `json:"useask"`
 	BatchInstall       bool     `json:"batchinstall"`
 	SingleLineResults  bool     `json:"singlelineresults"`
+	SeparateSources    bool     `json:"separatesources"`
 	Runtime            *Runtime `json:"-"`
 	Version            string   `json:"version"`
 }
@@ -216,6 +217,7 @@ func DefaultConfig(version string) *Configuration {
 		EditMenu:           false,
 		UseAsk:             false,
 		CombinedUpgrade:    false,
+		SeparateSources:    false,
 		Version:            version,
 	}
 }

+ 1 - 0
pkg/settings/parser/parser.go

@@ -446,6 +446,7 @@ func isArg(arg string) bool {
 	case "currentconfig":
 	case "singlelineresults":
 	case "doublelineresults":
+	case "separatesources", "noseparatesources":
 	default:
 		return false
 	}

+ 2 - 0
pkg/settings/runtime.go

@@ -7,6 +7,7 @@ import (
 
 	"github.com/Jguer/aur"
 
+	"github.com/Jguer/yay/v11/pkg/query"
 	"github.com/Jguer/yay/v11/pkg/settings/exe"
 	"github.com/Jguer/yay/v11/pkg/settings/parser"
 	"github.com/Jguer/yay/v11/pkg/vcs"
@@ -14,6 +15,7 @@ import (
 
 type Runtime struct {
 	Mode           parser.TargetMode
+	QueryBuilder   query.Builder
 	Version        string // current version of yay
 	SaveConfig     bool
 	CompletionPath string

+ 5 - 4
query.go

@@ -19,9 +19,9 @@ import (
 )
 
 // SyncSearch presents a query to the local repos and to the AUR.
-func syncSearch(ctx context.Context, pkgS []string, aurClient *aur.Client, dbExecutor db.Executor, verbose bool) error {
-	queryBuilder := query.NewSourceQueryBuilder(config.SortBy, config.Runtime.Mode, config.SearchBy, config.BottomUp, config.SingleLineResults)
-
+func syncSearch(ctx context.Context, pkgS []string, aurClient *aur.Client,
+	dbExecutor db.Executor, queryBuilder query.Builder, verbose bool,
+) error {
 	queryBuilder.Execute(ctx, dbExecutor, aurClient, pkgS)
 
 	searchMode := query.Minimal
@@ -209,7 +209,8 @@ func statistics(dbExecutor db.Executor) (res struct {
 	TotalSize    int64
 	pacmanCaches map[string]int64
 	yayCache     int64
-}) {
+},
+) {
 	for _, pkg := range dbExecutor.LocalPackages() {
 		res.TotalSize += pkg.ISize()
 		res.Totaln++