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(fmt.Sprintf( " %s choose • %s complete • %s again advances", primaryCombinedKeyLabel( m.keybindings.Input.PreviousCompletion, m.keybindings.Input.NextCompletion, ), primaryCombinedKeyLabel(m.keybindings.Input.NextField, m.keybindings.Input.Newline), primaryKeyLabel(m.keybindings.Input.NextField), ))} 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 }