mirror of
https://github.com/alibaba/open-code-review.git
synced 2026-07-11 10:19:02 +00:00
feat: 增加评论块与diff渲染
feat: 增加评论块与diff渲染
This commit is contained in:
parent
e99092a1f1
commit
d0e48329fa
2 changed files with 188 additions and 4 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
75
internal/suggestdiff/diff.go
Normal file
75
internal/suggestdiff/diff.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue