206 lines
5.4 KiB
Go
206 lines
5.4 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/charmbracelet/x/ansi"
|
|
)
|
|
|
|
type branchSuggestion struct {
|
|
branch RepositoryBranch
|
|
score int
|
|
}
|
|
|
|
func rankBranchSuggestions(
|
|
branches []RepositoryBranch,
|
|
query, current string,
|
|
now time.Time,
|
|
) []branchSuggestion {
|
|
query = strings.TrimSpace(strings.ToLower(query))
|
|
current = strings.ToLower(current)
|
|
suggestions := make([]branchSuggestion, 0, len(branches))
|
|
for _, branch := range branches {
|
|
name := strings.ToLower(branch.Name)
|
|
matchScore := 0
|
|
if query != "" {
|
|
var matches bool
|
|
matchScore, matches = fuzzyTermScore([]rune(name), []rune(query))
|
|
if !matches {
|
|
continue
|
|
}
|
|
switch {
|
|
case name == query:
|
|
matchScore += 50000
|
|
case strings.HasPrefix(name, query):
|
|
matchScore += 30000
|
|
case branchSegmentHasPrefix(name, query):
|
|
matchScore += 20000
|
|
}
|
|
}
|
|
|
|
score := matchScore * 10
|
|
if branch.IsDefault {
|
|
score += 9000
|
|
}
|
|
if name == current {
|
|
score += 7000
|
|
}
|
|
switch name {
|
|
case "main", "master":
|
|
score += 3500
|
|
case "develop", "development", "dev":
|
|
score += 2500
|
|
}
|
|
if strings.HasPrefix(name, "release/") || strings.HasPrefix(name, "release-") {
|
|
score += 1800
|
|
}
|
|
score += branchFreshnessScore(branch.UpdatedAt, now)
|
|
suggestions = append(suggestions, branchSuggestion{branch: branch, score: score})
|
|
}
|
|
sort.SliceStable(suggestions, func(i, j int) bool {
|
|
if suggestions[i].score != suggestions[j].score {
|
|
return suggestions[i].score > suggestions[j].score
|
|
}
|
|
if !suggestions[i].branch.UpdatedAt.Equal(suggestions[j].branch.UpdatedAt) {
|
|
return suggestions[i].branch.UpdatedAt.After(suggestions[j].branch.UpdatedAt)
|
|
}
|
|
return strings.ToLower(suggestions[i].branch.Name) <
|
|
strings.ToLower(suggestions[j].branch.Name)
|
|
})
|
|
return suggestions
|
|
}
|
|
|
|
func branchSegmentHasPrefix(name, query string) bool {
|
|
for _, segment := range strings.FieldsFunc(name, func(value rune) bool {
|
|
return strings.ContainsRune("/._-", value)
|
|
}) {
|
|
if strings.HasPrefix(segment, query) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func branchFreshnessScore(updatedAt, now time.Time) int {
|
|
if updatedAt.IsZero() {
|
|
return 0
|
|
}
|
|
age := now.Sub(updatedAt)
|
|
if age < 0 {
|
|
age = 0
|
|
}
|
|
days := int(age / (24 * time.Hour))
|
|
return max(0, 3000-min(days, 3000))
|
|
}
|
|
|
|
func branchAgeLabel(updatedAt, now time.Time) string {
|
|
if updatedAt.IsZero() {
|
|
return "age unknown"
|
|
}
|
|
age := now.Sub(updatedAt)
|
|
if age < time.Hour {
|
|
return "updated recently"
|
|
}
|
|
if age < 24*time.Hour {
|
|
return fmt.Sprintf("updated %dh ago", int(age/time.Hour))
|
|
}
|
|
days := int(age / (24 * time.Hour))
|
|
if days < 30 {
|
|
return fmt.Sprintf("updated %dd ago", days)
|
|
}
|
|
months := days / 30
|
|
if months < 24 {
|
|
return fmt.Sprintf("updated %dmo ago", months)
|
|
}
|
|
return fmt.Sprintf("updated %dy ago", days/365)
|
|
}
|
|
|
|
func (m App) branchSuggestions() []branchSuggestion {
|
|
suggestions := rankBranchSuggestions(
|
|
m.prEditBranches,
|
|
m.prEditEditors[prEditBaseField].Text,
|
|
m.prEditOriginal.BaseRef,
|
|
time.Now(),
|
|
)
|
|
const maximumVisibleSuggestions = 6
|
|
if len(suggestions) > maximumVisibleSuggestions {
|
|
suggestions = suggestions[:maximumVisibleSuggestions]
|
|
}
|
|
return suggestions
|
|
}
|
|
|
|
func (m *App) moveBranchSuggestion(delta int) {
|
|
suggestions := m.branchSuggestions()
|
|
if len(suggestions) == 0 {
|
|
m.prEditBranchIndex = 0
|
|
return
|
|
}
|
|
m.prEditBranchIndex = (m.prEditBranchIndex + delta + len(suggestions)) % len(suggestions)
|
|
}
|
|
|
|
func (m *App) completeBranchSuggestion() bool {
|
|
suggestions := m.branchSuggestions()
|
|
if len(suggestions) == 0 {
|
|
return false
|
|
}
|
|
index := clamp(m.prEditBranchIndex, 0, len(suggestions)-1)
|
|
name := suggestions[index].branch.Name
|
|
editor := &m.prEditEditors[prEditBaseField]
|
|
if editor.Text == name {
|
|
return false
|
|
}
|
|
editor.Text = name
|
|
editor.Cursor = len([]rune(name))
|
|
m.prEditBranchIndex = 0
|
|
m.err = nil
|
|
return true
|
|
}
|
|
|
|
func (m App) branchCompletionLines(width int) []string {
|
|
width = max(1, width)
|
|
if m.prEditBranchesLoading {
|
|
return []string{dimStyle.Render(" loading repository branches…")}
|
|
}
|
|
if m.prEditBranchesError != "" {
|
|
message := " branch recommendations unavailable: " + m.prEditBranchesError
|
|
wrapped := ansi.Hardwrap(ansi.Wordwrap(message, width, ""), width, false)
|
|
var lines []string
|
|
for _, line := range strings.Split(wrapped, "\n") {
|
|
lines = append(lines, warnStyle.Render(line))
|
|
}
|
|
return lines
|
|
}
|
|
suggestions := m.branchSuggestions()
|
|
if len(suggestions) == 0 {
|
|
return []string{dimStyle.Render(" no matching repository branches")}
|
|
}
|
|
lines := []string{dimStyle.Render(" ctrl-p/n choose • tab/enter complete • tab again advances")}
|
|
now := time.Now()
|
|
for index, suggestion := range suggestions {
|
|
prefix := " "
|
|
if index == clamp(m.prEditBranchIndex, 0, len(suggestions)-1) {
|
|
prefix = " ▶ "
|
|
}
|
|
suffix := branchAgeLabel(suggestion.branch.UpdatedAt, now)
|
|
if suggestion.branch.IsDefault {
|
|
suffix = "default • " + suffix
|
|
}
|
|
if suggestion.branch.Name == m.prEditOriginal.BaseRef {
|
|
suffix = "current • " + suffix
|
|
}
|
|
available := max(1, width-len([]rune(prefix))-len([]rune(suffix))-2)
|
|
name := ansi.Truncate(suggestion.branch.Name, available, "…")
|
|
line := prefix + name + strings.Repeat(" ", max(1, available-ansi.StringWidth(name)+1)) + dimStyle.Render(suffix)
|
|
if strings.HasPrefix(prefix, " ▶") {
|
|
line = titleStyle.Render(prefix+name) +
|
|
strings.Repeat(" ", max(1, available-ansi.StringWidth(name)+1)) +
|
|
dimStyle.Render(suffix)
|
|
}
|
|
lines = append(lines, line)
|
|
}
|
|
return lines
|
|
}
|