fix: reviewers not correctly shown on dashboard edit screen
This commit is contained in:
202
pr_editor.go
202
pr_editor.go
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user