Files
diple/branch_completion.go

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