Files
diple/suggestions.go

226 lines
5.6 KiB
Go

package main
import (
"strings"
"github.com/charmbracelet/x/ansi"
)
type parsedCommentBody struct {
Prose string
Suggestions []string
}
type codeRange struct {
Start int
End int
}
func parseCommentBody(body string) parsedCommentBody {
var (
result parsedCommentBody
prose []string
suggestion []string
openingLine string
inSuggestion bool
)
for _, line := range strings.Split(body, "\n") {
trimmed := strings.TrimSpace(line)
if !inSuggestion && isSuggestionFence(trimmed) {
inSuggestion = true
openingLine = line
suggestion = nil
continue
}
if inSuggestion && trimmed == "```" {
result.Suggestions = append(result.Suggestions, strings.Join(suggestion, "\n"))
inSuggestion = false
openingLine = ""
continue
}
if inSuggestion {
suggestion = append(suggestion, line)
} else {
prose = append(prose, line)
}
}
if inSuggestion {
prose = append(prose, openingLine)
prose = append(prose, suggestion...)
}
result.Prose = strings.TrimSpace(strings.Join(prose, "\n"))
return result
}
func isSuggestionFence(line string) bool {
if !strings.HasPrefix(line, "```suggestion") {
return false
}
suffix := strings.TrimPrefix(line, "```suggestion")
return suffix == "" || strings.HasPrefix(suffix, ":")
}
func reviewedSourceLines(thread ReviewThread, comment ReviewComment) []string {
start, end := commentReviewAnchor(thread, comment)
parsed := parseDiff(comment.DiffHunk, start, end, thread.DiffSide)
lines := make([]string, 0, end-start+1)
for _, line := range parsed {
if !line.selected {
continue
}
lines = append(lines, sourceFromDiffLine(line.raw))
}
return lines
}
func sourceFromDiffLine(raw string) string {
if strings.HasPrefix(raw, "+") || strings.HasPrefix(raw, "-") || strings.HasPrefix(raw, " ") {
raw = raw[1:]
}
return strings.ReplaceAll(raw, "\t", " ")
}
func normalizeSuggestion(removed []string, replacement string) ([]string, []string) {
added := []string(nil)
if replacement != "" {
added = strings.Split(replacement, "\n")
for i := range added {
added[i] = strings.ReplaceAll(added[i], "\t", " ")
}
}
padding := commonSourceIndent(removed, added)
return trimSourceIndent(removed, padding), trimSourceIndent(added, padding)
}
func commonSourceIndent(groups ...[]string) int {
padding := -1
for _, lines := range groups {
for _, line := range lines {
if strings.TrimSpace(line) == "" {
continue
}
indent := len(line) - len(strings.TrimLeft(line, " "))
if padding == -1 || indent < padding {
padding = indent
}
}
}
return max(0, padding)
}
func trimSourceIndent(lines []string, padding int) []string {
result := make([]string, len(lines))
for i, line := range lines {
result[i] = trimIndent(line, padding)
}
return result
}
func suggestionChangedRanges(removed, added []string) ([]codeRange, []codeRange) {
removedRanges := make([]codeRange, len(removed))
addedRanges := make([]codeRange, len(added))
matches := matchingLinePairs(removed, added)
matches = append(matches, [2]int{len(removed), len(added)})
oldStart, newStart := 0, 0
for _, match := range matches {
oldEnd, newEnd := match[0], match[1]
blockSize := max(oldEnd-oldStart, newEnd-newStart)
for offset := 0; offset < blockSize; offset++ {
oldIndex, newIndex := oldStart+offset, newStart+offset
switch {
case oldIndex < oldEnd && newIndex < newEnd:
removedRanges[oldIndex], addedRanges[newIndex] = changedRange(
removed[oldIndex], added[newIndex],
)
case oldIndex < oldEnd:
removedRanges[oldIndex] = fullCodeRange(removed[oldIndex])
case newIndex < newEnd:
addedRanges[newIndex] = fullCodeRange(added[newIndex])
}
}
if oldEnd < len(removed) && newEnd < len(added) {
oldStart, newStart = oldEnd+1, newEnd+1
} else {
oldStart, newStart = oldEnd, newEnd
}
}
return removedRanges, addedRanges
}
func matchingLinePairs(removed, added []string) [][2]int {
dp := make([][]int, len(removed)+1)
for i := range dp {
dp[i] = make([]int, len(added)+1)
}
for i := len(removed) - 1; i >= 0; i-- {
for j := len(added) - 1; j >= 0; j-- {
if removed[i] == added[j] {
dp[i][j] = dp[i+1][j+1] + 1
} else {
dp[i][j] = max(dp[i+1][j], dp[i][j+1])
}
}
}
var matches [][2]int
for i, j := 0, 0; i < len(removed) && j < len(added); {
switch {
case removed[i] == added[j]:
matches = append(matches, [2]int{i, j})
i++
j++
case dp[i+1][j] >= dp[i][j+1]:
i++
default:
j++
}
}
return matches
}
func changedRange(oldLine, newLine string) (codeRange, codeRange) {
oldRunes, newRunes := []rune(oldLine), []rune(newLine)
prefix := 0
for prefix < len(oldRunes) && prefix < len(newRunes) && oldRunes[prefix] == newRunes[prefix] {
prefix++
}
oldSuffix, newSuffix := len(oldRunes), len(newRunes)
for oldSuffix > prefix && newSuffix > prefix &&
oldRunes[oldSuffix-1] == newRunes[newSuffix-1] {
oldSuffix--
newSuffix--
}
return codeRange{
Start: ansi.StringWidth(string(oldRunes[:prefix])),
End: ansi.StringWidth(string(oldRunes[:oldSuffix])),
}, codeRange{
Start: ansi.StringWidth(string(newRunes[:prefix])),
End: ansi.StringWidth(string(newRunes[:newSuffix])),
}
}
func fullCodeRange(line string) codeRange {
return codeRange{End: ansi.StringWidth(line)}
}
func commentReviewAnchor(thread ReviewThread, comment ReviewComment) (int, int) {
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
}
start = thread.StartLine
if start == 0 {
start = thread.Line
}
return start, thread.Line
}