Make code suggestions work aswell

This commit is contained in:
2026-07-27 14:04:41 +02:00
parent b0ac2f3e9a
commit 6bfe248388
4 changed files with 521 additions and 26 deletions

141
tui.go
View File

@@ -437,7 +437,15 @@ func (m App) threadDetail(width, height int) string {
rendered := make([]string, 0, len(visible))
innerWidth := max(1, width-2)
for _, line := range visible {
renderedLine := ansi.Truncate(line.fixed+line.text, innerWidth, "…")
railWidth := ansi.StringWidth(line.rail)
contentWidth := max(1, innerWidth-railWidth)
var renderedLine string
if line.suggestionChange != 0 {
renderedLine = suggestionHighlight(line.fixed, line.text, contentWidth, line.suggestionChange)
} else {
renderedLine = ansi.Truncate(line.fixed+line.text, contentWidth, "…")
}
renderedLine = line.rail + renderedLine
if line.selected {
renderedLine = selectedBackground(renderedLine, innerWidth)
}
@@ -447,9 +455,11 @@ func (m App) threadDetail(width, height int) string {
}
type detailLine struct {
text string
fixed string
selected bool
text string
fixed string
rail string
selected bool
suggestionChange byte
}
func (m App) detailLines(width int) []detailLine {
@@ -481,9 +491,40 @@ func (m App) detailLines(width int) []detailLine {
}
}
for _, comment := range thread.Comments {
lines = append(lines, detailLine{}, detailLine{text: authorStyle(comment.Author).Render("@"+comment.Author) + " " + dimStyle.Render(comment.CreatedAt.Local().Format("2006-01-02 15:04"))})
for _, commentLine := range strings.Split(wrap(comment.Body, max(10, width-4)), "\n") {
lines = append(lines, detailLine{text: commentLine})
content := parseCommentBody(comment.Body)
rail := lipgloss.NewStyle().Foreground(authorColor(comment.Author)).Render("│ ")
lines = append(lines, detailLine{}, detailLine{
rail: rail,
text: authorStyle(comment.Author).Render("@"+comment.Author) + " " +
dimStyle.Render(comment.CreatedAt.Local().Format("2006-01-02 15:04")),
})
if content.Prose != "" {
for _, commentLine := range strings.Split(wrap(content.Prose, max(10, width-4)), "\n") {
lines = append(lines, detailLine{rail: rail, text: commentLine})
}
}
for suggestionIndex, suggestion := range content.Suggestions {
removed, added := normalizeSuggestion(reviewedSourceLines(thread, comment), suggestion)
removedRanges, addedRanges := suggestionChangedRanges(removed, added)
label := "suggested change"
if len(content.Suggestions) > 1 {
label = fmt.Sprintf("suggested change %d/%d", suggestionIndex+1, len(content.Suggestions))
}
lines = append(lines, detailLine{rail: rail}, detailLine{rail: rail, text: dimStyle.Render(label)})
for sourceIndex, source := range removed {
lines = append(lines, addCommentRail(
wrapSuggestionLine(
thread.Path, source, '-', removedRanges[sourceIndex], max(1, width-4),
), rail,
)...)
}
for sourceIndex, source := range added {
lines = append(lines, addCommentRail(
wrapSuggestionLine(
thread.Path, source, '+', addedRanges[sourceIndex], max(1, width-4),
), rail,
)...)
}
}
}
if thread.IsTruncated {
@@ -520,6 +561,53 @@ func wrapDiffLine(line highlightedDiffLine, width int) []detailLine {
return result
}
func wrapSuggestionLine(path, source string, change byte, changed codeRange, width int) []detailLine {
marker := badStyle.Render(string(change))
if change == '+' {
marker = okStyle.Render(string(change))
}
code := highlightedSource(lexerForPath(path), source)
code = highlightCodeRange(code, changed, change)
lines := wrapDiffLine(highlightedDiffLine{
gutter: marker + " ",
code: code,
}, width)
for i := range lines {
lines[i].suggestionChange = change
}
return lines
}
func highlightCodeRange(code string, changed codeRange, change byte) string {
if changed.End <= changed.Start {
return code
}
totalWidth := ansi.StringWidth(code)
start := clamp(changed.Start, 0, totalWidth)
end := clamp(changed.End, start, totalWidth)
if end <= start {
return code
}
background := "\x1b[48;2;55;0;0m"
if change == '+' {
background = "\x1b[48;2;0;55;0m"
}
const reset = "\x1b[0m"
before := ansi.Cut(code, 0, start)
middle := ansi.Cut(code, start, end)
after := ansi.Cut(code, end, totalWidth)
middle = strings.ReplaceAll(middle, reset, reset+background)
return before + background + middle + reset + after
}
func addCommentRail(lines []detailLine, rail string) []detailLine {
for i := range lines {
lines[i].rail = rail
}
return lines
}
func wrapCodeWithIndent(code string, width int) []string {
if width <= 0 || ansi.StringWidth(code) <= width {
return []string{code}
@@ -580,25 +668,9 @@ func isCodeBreakpoint(r rune) bool {
func reviewAnchor(thread ReviewThread) (int, int) {
if len(thread.Comments) > 0 {
comment := thread.Comments[0]
end := comment.OriginalLine
start := comment.OriginalStartLine
if end == 0 {
end = comment.Line
start = comment.StartLine
}
if end > 0 {
if start == 0 {
start = end
}
return start, end
}
return commentReviewAnchor(thread, thread.Comments[0])
}
start := thread.StartLine
if start == 0 {
start = thread.Line
}
return start, thread.Line
return commentReviewAnchor(thread, ReviewComment{})
}
func shortOID(oid string) string {
@@ -618,6 +690,25 @@ func selectedBackground(line string, width int) string {
return background + line + reset
}
func suggestionHighlight(gutter, code string, width int, change byte) string {
background := "\x1b[48;5;52m"
if change == '+' {
background = "\x1b[48;5;22m"
}
const reset = "\x1b[0m"
gutter = ansi.Truncate(gutter, width, "")
gutter = strings.ReplaceAll(gutter, reset, reset+background)
codeWidth := max(0, width-ansi.StringWidth(gutter))
code = ansi.Truncate(code, codeWidth, "")
code = strings.ReplaceAll(code, reset, reset+background)
content := background + gutter + code + reset
padding := max(0, width-ansi.StringWidth(gutter)-ansi.StringWidth(code))
if padding > 0 {
content += background + strings.Repeat(" ", padding) + reset
}
return content
}
var authorPalette = []lipgloss.Color{
"#61AFEF", "#C678DD", "#56B6C2", "#E5C07B",
"#E06C75", "#98C379", "#D19A66", "#7FC8FF",