fix: reviewers not correctly shown on dashboard edit screen

This commit is contained in:
2026-07-30 11:26:21 +02:00
parent bcad515698
commit e045bd39b2
7 changed files with 549 additions and 15 deletions

View File

@@ -168,6 +168,15 @@ func (m App) updatePREditInput(key tea.KeyMsg) (tea.Model, tea.Cmd) {
}
return m, nil
}
if key.Type == tea.KeySpace && m.prEditField == prEditReviewersField {
if m.startNextReviewer() {
m.ensurePREditCursorVisible()
return m, m.queuePREditDraft()
}
// GitHub usernames cannot contain spaces. Ignore a space until the
// current entry is an exact eligible reviewer.
return m, nil
}
switch k {
case "ctrl+s":
@@ -280,7 +289,7 @@ func (m App) positionPREditHardwareCursor(scroll, viewportHeight int) {
if m.cursorOutput == nil {
return
}
editor := m.prEditEditors[m.prEditField]
editor := m.prEditDisplayEditor(m.prEditField, m.prEditEditorWidth())
if editor.Mode != textEditorInsert {
return
}
@@ -524,7 +533,9 @@ func (m App) dashboardEditLayout() ([]string, int) {
start := len(lines)
lines = append(lines, m.prEditFieldLines(label, field, width)...)
if m.prEditField == field {
cursorLine = start + 1 + editorCursorVisualLine(m.prEditEditors[field], max(1, width-4))
cursorLine = start + 1 + editorCursorVisualLine(
m.prEditDisplayEditor(field, max(1, width-4)), max(1, width-4),
)
}
}
appendField("title", prEditTitleField)
@@ -538,6 +549,9 @@ func (m App) dashboardEditLayout() ([]string, int) {
func (m App) prEditFieldLines(label string, field, width int) []string {
active := m.prEditField == field
editor := m.prEditEditors[field]
if field == prEditReviewersField {
label += " (pending requests editable)"
}
prefix := " "
if active {
prefix = "▶ "
@@ -551,7 +565,7 @@ func (m App) prEditFieldLines(label string, field, width int) []string {
labelLine = titleStyle.Render(prefix + label)
}
textWidth := max(1, width-4)
rendered := renderTextEditor(editor, textWidth, active)
rendered := renderTextEditor(m.prEditDisplayEditor(field, textWidth), textWidth, active)
lines := []string{labelLine}
for _, line := range rendered {
if line.active {
@@ -572,6 +586,188 @@ func (m App) prEditFieldLines(label string, field, width int) []string {
return lines
}
func (m App) prEditDisplayEditor(field, width int) textEditor {
editor := m.prEditEditors[field]
if field != prEditReviewersField {
return editor
}
editor, editableStyles := m.prEditEligibleReviewerDisplay(editor)
prefix, protectedStyles := m.prEditReadOnlyReviewerPrefix(width)
if prefix == "" {
editor.protectedStyles = editableStyles
return editor
}
offset := len([]rune(prefix))
editor.Text = prefix + editor.Text
editor.Cursor += offset
editor.visualAnchor += offset
editor.protectedPrefix = offset
editor.protectedStyles = append(protectedStyles, shiftedEditorStyles(editableStyles, offset)...)
return editor
}
func (m App) prEditEligibleReviewerDisplay(editor textEditor) (textEditor, []editorProtectedStyle) {
type eligibleToken struct {
start, end int
login string
insertAt bool
}
eligible := make(map[string]string, len(m.prEditUsers))
for _, user := range m.prEditUsers {
if user.CanReview && !strings.EqualFold(user.Login, m.details.Author) {
eligible[strings.ToLower(user.Login)] = user.Login
}
}
runes := []rune(editor.Text)
var tokens []eligibleToken
for segmentStart := 0; segmentStart <= len(runes); {
segmentEnd := segmentStart
for segmentEnd < len(runes) && runes[segmentEnd] != ',' {
segmentEnd++
}
start, end := segmentStart, segmentEnd
for start < end && (runes[start] == ' ' || runes[start] == '\t') {
start++
}
for end > start && (runes[end-1] == ' ' || runes[end-1] == '\t') {
end--
}
hasAt := start < end && runes[start] == '@'
loginStart := start
if hasAt {
loginStart++
}
login := string(runes[loginStart:end])
if canonical, ok := eligible[strings.ToLower(login)]; ok && login != "" {
tokens = append(tokens, eligibleToken{
start: start, end: end, login: canonical, insertAt: !hasAt,
})
}
if segmentEnd == len(runes) {
break
}
segmentStart = segmentEnd + 1
}
if len(tokens) == 0 {
return editor, nil
}
insertions := make(map[int]bool)
for _, token := range tokens {
if token.insertAt {
insertions[token.start] = true
}
}
displayRunes := make([]rune, 0, len(runes)+len(insertions))
for index, value := range runes {
if insertions[index] {
displayRunes = append(displayRunes, '@')
}
displayRunes = append(displayRunes, value)
}
if insertions[len(runes)] {
displayRunes = append(displayRunes, '@')
}
mappedPosition := func(position int) int {
mapped := position
for insertion := range insertions {
if insertion <= position {
mapped++
}
}
return mapped
}
var styles []editorProtectedStyle
for _, token := range tokens {
start := mappedPosition(token.start)
if token.insertAt {
start--
}
styles = append(styles, editorProtectedStyle{
start: start,
end: start + 1 + len([]rune(token.login)),
color: string(authorColor(token.login)),
})
}
editor.Text = string(displayRunes)
editor.Cursor = mappedPosition(editor.Cursor)
editor.visualAnchor = mappedPosition(editor.visualAnchor)
return editor, styles
}
func shiftedEditorStyles(styles []editorProtectedStyle, offset int) []editorProtectedStyle {
shifted := make([]editorProtectedStyle, len(styles))
for index, style := range styles {
style.start += offset
style.end += offset
shifted[index] = style
}
return shifted
}
func (m App) prEditReadOnlyReviewerPrefix(width int) (string, []editorProtectedStyle) {
editable := parseLoginList(m.prEditEditors[prEditReviewersField].Text)
editableSet := make(map[string]bool, len(editable))
for _, login := range editable {
editableSet[strings.ToLower(login)] = true
}
requestedSet := make(map[string]bool, len(m.details.RequestedReviewers))
for _, login := range m.details.RequestedReviewers {
requestedSet[strings.ToLower(login)] = true
}
var tokens []string
var readOnlyReviewers []Reviewer
for _, reviewer := range m.details.Reviewers {
key := strings.ToLower(reviewer.Login)
if editableSet[key] ||
(requestedSet[key] && reviewer.State == "REVIEW_REQUESTED") {
continue
}
state := strings.ToLower(strings.ReplaceAll(reviewer.State, "_", " "))
if state == "" {
state = "reviewed"
}
tokens = append(tokens, "[@"+reviewer.Login+" · "+state+"]")
readOnlyReviewers = append(readOnlyReviewers, reviewer)
}
if len(tokens) == 0 {
return "", nil
}
width = max(1, width)
var prefix strings.Builder
var styles []editorProtectedStyle
lineWidth := 0
runeOffset := 0
for index, token := range tokens {
tokenWidth := ansi.StringWidth(token)
if lineWidth > 0 && lineWidth+1+tokenWidth > width {
prefix.WriteByte('\n')
lineWidth = 0
runeOffset++
}
if lineWidth > 0 {
prefix.WriteByte(' ')
lineWidth++
runeOffset++
}
prefix.WriteString(token)
login := readOnlyReviewers[index].Login
styles = append(styles, editorProtectedStyle{
start: runeOffset + 1,
end: runeOffset + 2 + len([]rune(login)),
color: string(darkenColor(authorColor(login))),
})
lineWidth += tokenWidth
runeOffset += len([]rune(token))
}
if lineWidth+2 >= width {
prefix.WriteByte('\n')
} else {
prefix.WriteString(" ")
}
return prefix.String(), styles
}
func (m *App) ensurePREditCursorVisible() {
if m.writeMode != writePREdit {
return