feat: 增加评论块与diff渲染

feat: 增加评论块与diff渲染
This commit is contained in:
kite 2026-04-30 17:27:31 +08:00
parent e99092a1f1
commit d0e48329fa
2 changed files with 188 additions and 4 deletions

View file

@ -4,8 +4,10 @@ import (
"encoding/json"
"fmt"
"os"
"strings"
"github.com/open-code-review/open-code-review/internal/model"
"github.com/open-code-review/open-code-review/internal/suggestdiff"
)
func outputText(comments []model.LlmComment) {
@ -14,13 +16,120 @@ func outputText(comments []model.LlmComment) {
return
}
for _, c := range comments {
fmt.Printf("--- %s:%d-%d ---\n", c.Path, c.StartLine, c.EndLine)
fmt.Println(c.Content)
if c.SuggestionCode != "" {
fmt.Printf("\n```suggestion\n%s\n```\n", c.SuggestionCode)
renderComment(c)
}
}
func renderComment(comment model.LlmComment) {
lines := buildDiffLines(comment)
if len(lines) == 0 && comment.Content == "" {
return
}
fmt.Printf("\n\033[2m─── %s:%d-%d ───\033[0m\n", comment.Path, comment.StartLine, comment.EndLine)
if comment.Content != "" {
for _, ln := range wrapByRunes(comment.Content, 100) {
fmt.Printf("%s\n", ln)
}
fmt.Println()
}
if len(lines) > 0 {
for _, dl := range lines {
switch dl.Type {
case suggestdiff.DiffAdded:
printDiffLine("+", dl.Content, "\033[92m", "\033[48;2;0;60;0m")
case suggestdiff.DiffDeleted:
printDiffLine("-", dl.Content, "\033[91m", "\033[48;2;70;0;0m")
case suggestdiff.DiffContext:
printDiffLine(" ", dl.Content, "\033[2m", "\033[48;2;38;38;38m")
}
}
}
fmt.Println()
}
// printDiffLine renders a single diff line with colored prefix and background on content.
func printDiffLine(prefix, content, fgColor, bgColor string) {
fmt.Printf("%s%s%s %s%s\033[0m\n", fgColor+bgColor, prefix, "\033[0m"+bgColor, content, "\033[0m")
}
// wrapByRunes splits text into lines that fit within maxWidth **rune** columns.
// Respects existing newlines and wraps at word boundaries.
func wrapByRunes(text string, maxW int) []string {
if text == "" {
return nil
}
var result []string
for _, para := range strings.Split(text, "\n") {
result = append(result, wrapSingleRuneLine(para, maxW)...)
}
return result
}
// wrapSingleRuneLine breaks one paragraph (no newlines) into rune-width-constrained lines.
func wrapSingleRuneLine(line string, maxW int) []string {
runes := []rune(line)
if visibleRunesLen(runes) <= maxW {
return []string{line}
}
var result []string
for len(runes) > 0 {
cut := runeWrapCut(runes, maxW)
result = append(result, string(runes[:cut]))
runes = runes[cut:]
// trim leading spaces of next segment
for len(runes) > 0 && runes[0] == ' ' {
runes = runes[1:]
}
}
return result
}
// runeWrapCut returns a rune index suitable for breaking the line at ~maxW display width.
func runeWrapCut(runes []rune, maxW int) int {
if visibleRunesLen(runes) <= maxW {
return len(runes)
}
best := maxW
if best >= len(runes) {
return len(runes)
}
for i := best; i > 0; i-- {
if runes[i] == ' ' || runes[i] == '\t' {
return i
}
}
return best
}
func visibleRunesLen(runes []rune) int {
n := 0
for _, r := range runes {
if r >= 32 && r != 127 {
n++
}
}
return n
}
func splitToLines(s string) []string {
lines := strings.Split(strings.ReplaceAll(s, "\r\n", "\n"), "\n")
if len(lines) > 0 && lines[len(lines)-1] == "" {
lines = lines[:len(lines)-1]
}
return lines
}
func buildDiffLines(comment model.LlmComment) []suggestdiff.DiffLine {
if comment.SuggestionCode == "" || comment.ExistingCode == "" {
return nil
}
oldLines := splitToLines(comment.ExistingCode)
newLines := splitToLines(comment.SuggestionCode)
return suggestdiff.ComputeLineDiff(oldLines, newLines)
}
func outputJSON(comments []model.LlmComment) error {

View file

@ -0,0 +1,75 @@
// Package suggestdiff provides line-level diff computation between code snippets,
// used for CLI rendering of review suggestions with ANSI color codes.
package suggestdiff
import "strings"
// DiffLineType marks a line as context, added, or deleted.
type DiffLineType int
const (
DiffContext DiffLineType = iota
DiffAdded
DiffDeleted
)
// DiffLine is a single line in the diff result.
type DiffLine struct {
Type DiffLineType
Content string
}
// ComputeLineDiff returns a line-level diff between oldLines and newLines.
// Uses Myers-style LCS to find common subsequences, then emits context/added/deleted lines.
func ComputeLineDiff(oldLines, newLines []string) []DiffLine {
m, n := len(oldLines), len(newLines)
if m == 0 && n == 0 {
return nil
}
// LCS DP table
lcs := make([][]int, m+1)
for i := range lcs {
lcs[i] = make([]int, n+1)
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if strings.EqualFold(strings.TrimSpace(oldLines[i-1]), strings.TrimSpace(newLines[j-1])) {
lcs[i][j] = lcs[i-1][j-1] + 1
} else {
lcs[i][j] = max(lcs[i-1][j], lcs[i][j-1])
}
}
}
// Backtrack to produce diff
var result []DiffLine
i, j := m, n
back := make([]DiffLine, 0, max(m, n)*2)
for i > 0 || j > 0 {
if i > 0 && j > 0 && strings.EqualFold(strings.TrimSpace(oldLines[i-1]), strings.TrimSpace(newLines[j-1])) {
back = append(back, DiffLine{Type: DiffContext, Content: oldLines[i-1]})
i--
j--
} else if j > 0 && (i == 0 || lcs[i][j-1] >= lcs[i-1][j]) {
back = append(back, DiffLine{Type: DiffAdded, Content: newLines[j-1]})
j--
} else {
back = append(back, DiffLine{Type: DiffDeleted, Content: oldLines[i-1]})
i--
}
}
// Reverse
for idx := len(back) - 1; idx >= 0; idx-- {
result = append(result, back[idx])
}
return result
}
func max(a, b int) int {
if a > b {
return a
}
return b
}