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

178
suggestions_test.go Normal file
View File

@@ -0,0 +1,178 @@
package main
import (
"strings"
"testing"
"github.com/charmbracelet/x/ansi"
)
func TestParseCommentBodyExtractsSuggestionFences(t *testing.T) {
body := "Use the clearer name:\n\n```suggestion:-0+0\n better_name = value\n```\n\nThis also matches the API."
got := parseCommentBody(body)
if len(got.Suggestions) != 1 || got.Suggestions[0] != " better_name = value" {
t.Fatalf("suggestions = %#v", got.Suggestions)
}
if strings.Contains(got.Prose, "```") || !strings.Contains(got.Prose, "Use the clearer name") ||
!strings.Contains(got.Prose, "matches the API") {
t.Fatalf("prose = %q", got.Prose)
}
}
func TestReviewedSourceLinesUseHistoricalSelection(t *testing.T) {
comment := ReviewComment{
DiffHunk: "@@ -38,5 +38,5 @@\n context\n old_name = value\n return old_name\n context\n context",
OriginalStartLine: 39,
OriginalLine: 40,
}
got := reviewedSourceLines(ReviewThread{DiffSide: "RIGHT"}, comment)
want := []string{"old_name = value", "return old_name"}
if strings.Join(got, "\n") != strings.Join(want, "\n") {
t.Fatalf("reviewed source = %#v, want %#v", got, want)
}
}
func TestNormalizeSuggestionPreservesRelativeIndent(t *testing.T) {
removed, added := normalizeSuggestion(
[]string{" if old:", " run_old()"},
" if new:\n run_new()",
)
if strings.Join(removed, "\n") != "if old:\n run_old()" {
t.Fatalf("removed = %#v", removed)
}
if strings.Join(added, "\n") != "if new:\n run_new()" {
t.Fatalf("added = %#v", added)
}
}
func TestDetailRendersSuggestionAsRemovalAndAddition(t *testing.T) {
m := NewApp(nil, "o", "r", false, 50, 10)
m.width = 100
m.details = PRDetails{Threads: []ReviewThread{{
Path: "example.py",
Line: 10,
DiffSide: "RIGHT",
Comments: []ReviewComment{{
Author: "reviewer",
Body: "Use this instead:\n```suggestion\nnew_value = compute()\nreturn new_value\n```",
DiffHunk: "@@ -10,2 +10,2 @@\nold_value = compute()\nreturn old_value",
OriginalLine: 11,
OriginalStartLine: 10,
}},
}}}
var (
removed, added int
rendered strings.Builder
)
for _, line := range m.detailLines(60) {
rendered.WriteString(ansi.Strip(line.rail + line.fixed + line.text))
rendered.WriteByte('\n')
switch line.suggestionChange {
case '-':
removed++
case '+':
added++
}
}
if removed != 2 || added != 2 {
t.Fatalf("suggestion rows: removed=%d added=%d\n%s", removed, added, rendered.String())
}
if strings.Contains(rendered.String(), "```suggestion") ||
!strings.Contains(rendered.String(), "│ @reviewer") ||
!strings.Contains(rendered.String(), "│ suggested change") {
t.Fatalf("suggestion was not rendered structurally:\n%s", rendered.String())
}
}
func TestSuggestionBackgroundIsDirectionalWithoutTextUnderline(t *testing.T) {
removed := suggestionHighlight(" - ", "old", 12, '-')
added := suggestionHighlight(" + ", "new", 12, '+')
if !strings.Contains(removed, "\x1b[48;5;52m") ||
!strings.Contains(added, "\x1b[48;5;22m") {
t.Fatalf("suggestion decorations missing: removed=%q added=%q", removed, added)
}
if strings.Contains(removed, "\x1b[4m") || strings.Contains(added, "\x1b[4m") ||
strings.Contains(removed, "\x1b[4;") || strings.Contains(added, "\x1b[4;") ||
strings.Contains(removed, "\x1b[58;") || strings.Contains(added, "\x1b[58;") {
t.Fatal("suggestion rendering still enables terminal underlining")
}
if ansi.StringWidth(removed) != 12 || ansi.StringWidth(added) != 12 {
t.Fatal("suggestion backgrounds do not fill the row")
}
if !strings.Contains(removed, "\x1b[0m\x1b[48;5;52m ") ||
!strings.Contains(added, "\x1b[0m\x1b[48;5;22m ") {
t.Fatal("padded row remainder is still underlined")
}
}
func TestMultipleSuggestionsStayInsideCommentBlock(t *testing.T) {
m := NewApp(nil, "o", "r", false, 50, 10)
m.width = 100
m.details = PRDetails{Threads: []ReviewThread{{
Path: "main.go", Line: 1, DiffSide: "RIGHT",
Comments: []ReviewComment{{
Author: "alice", OriginalLine: 1,
DiffHunk: "@@ -1 +1 @@\nold",
Body: "```suggestion\nfirst\n```\n```suggestion\nsecond\n```",
}},
}}}
var rendered strings.Builder
for _, line := range m.detailLines(60) {
rendered.WriteString(ansi.Strip(line.rail + line.fixed + line.text))
rendered.WriteByte('\n')
}
if !strings.Contains(rendered.String(), "│ @alice") ||
!strings.Contains(rendered.String(), "│ suggested change 1/2") ||
!strings.Contains(rendered.String(), "│ suggested change 2/2") {
t.Fatalf("suggestions escaped the comment block:\n%s", rendered.String())
}
}
func TestSuggestionGutterHasNoLeadingPadding(t *testing.T) {
lines := wrapSuggestionLine("main.go", "replacement()", '+', fullCodeRange("replacement()"), 30)
if len(lines) == 0 || ansi.Strip(lines[0].fixed) != "+ " {
t.Fatalf("suggestion gutter = %q, want compact marker", ansi.Strip(lines[0].fixed))
}
}
func TestSuggestionChangedRangesAlignInsertedLines(t *testing.T) {
removed := []string{"def setup():", " if ready:", " run()"}
added := []string{"def setup():", " super().setup()", " if ready:", " run()"}
removedRanges, addedRanges := suggestionChangedRanges(removed, added)
for i, changed := range removedRanges {
if changed.End != changed.Start {
t.Fatalf("unchanged removed line %d marked as changed: %#v", i, changed)
}
}
for _, index := range []int{0, 2, 3} {
if changed := addedRanges[index]; changed.End != changed.Start {
t.Fatalf("unchanged added line %d marked as changed: %#v", index, changed)
}
}
if addedRanges[1] != fullCodeRange(added[1]) {
t.Fatalf("inserted line range = %#v", addedRanges[1])
}
}
func TestSuggestionChangedRangeIsCharacterPrecise(t *testing.T) {
removed, added := changedRange("value = old_name()", "value = new_name()")
if got := "value = old_name()"[removed.Start:removed.End]; got != "old" {
t.Fatalf("removed range selected %q", got)
}
if got := "value = new_name()"[added.Start:added.End]; got != "new" {
t.Fatalf("added range selected %q", got)
}
}
func TestChangedSnippetGetsDarkerBackground(t *testing.T) {
code := highlightedSource("go", "value := oldName()")
decorated := highlightCodeRange(code, codeRange{Start: 9, End: 16}, '-')
if !strings.Contains(decorated, "\x1b[48;2;55;0;0m") {
t.Fatalf("darker intra-line background missing: %q", decorated)
}
if ansi.Strip(decorated) != "value := oldName()" {
t.Fatalf("highlight altered source: %q", ansi.Strip(decorated))
}
}