优化消息里读/写内容显示样式
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { MessageTurn } from "./types"
|
||||
import { normalizeToolName } from "./tool-call-normalization"
|
||||
import { generateUnifiedDiff } from "./unified-diff-generator"
|
||||
|
||||
export type FileOperation = "read" | "edit" | "write" | "apply_patch"
|
||||
|
||||
@@ -266,32 +267,12 @@ function countDiffLines(text: string): DiffStat {
|
||||
return { additions, deletions }
|
||||
}
|
||||
|
||||
function createHunkHeader(oldLineCount: number, newLineCount: number): string {
|
||||
const oldStart = oldLineCount === 0 ? 0 : 1
|
||||
const newStart = newLineCount === 0 ? 0 : 1
|
||||
return `@@ -${oldStart},${oldLineCount} +${newStart},${newLineCount} @@`
|
||||
}
|
||||
|
||||
function buildUnifiedDiff(
|
||||
filePath: string,
|
||||
oldText: string,
|
||||
newText: string
|
||||
): string | null {
|
||||
if (!oldText && !newText) return null
|
||||
|
||||
const oldLines = oldText ? oldText.split("\n") : []
|
||||
const newLines = newText ? newText.split("\n") : []
|
||||
|
||||
const lines: string[] = [
|
||||
`--- a/${filePath}`,
|
||||
`+++ b/${filePath}`,
|
||||
createHunkHeader(oldLines.length, newLines.length),
|
||||
]
|
||||
|
||||
for (const line of oldLines) lines.push(`-${line}`)
|
||||
for (const line of newLines) lines.push(`+${line}`)
|
||||
|
||||
return lines.join("\n")
|
||||
return generateUnifiedDiff(oldText, newText, filePath)
|
||||
}
|
||||
|
||||
function parseEditChangeValue(value: unknown): EditChangePreview | null {
|
||||
|
||||
180
src/lib/unified-diff-generator.ts
Normal file
180
src/lib/unified-diff-generator.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { computeLineDiff, type DiffHunk } from "@/components/merge/merge-diff"
|
||||
|
||||
/**
|
||||
* Maximum product of line counts before falling back to naive diff.
|
||||
* Avoids O(n*m) LCS blowup for very large inputs.
|
||||
*/
|
||||
const LCS_PAIR_BUDGET = 200_000
|
||||
|
||||
/**
|
||||
* Generate a unified diff string from old and new text.
|
||||
*
|
||||
* Uses LCS-based line diff when within budget, falls back to
|
||||
* simple "all deletions then all additions" for very large inputs.
|
||||
*/
|
||||
export function generateUnifiedDiff(
|
||||
oldText: string,
|
||||
newText: string,
|
||||
filePath?: string,
|
||||
contextLines: number = 3
|
||||
): string | null {
|
||||
if (!oldText && !newText) return null
|
||||
if (oldText === newText) return null
|
||||
|
||||
const oldLines = oldText ? splitLines(oldText) : []
|
||||
const newLines = newText ? splitLines(newText) : []
|
||||
|
||||
const path = filePath ?? "file"
|
||||
const header = `--- a/${path}\n+++ b/${path}`
|
||||
|
||||
// Performance gate: fall back to naive diff for large inputs
|
||||
if (oldLines.length * newLines.length > LCS_PAIR_BUDGET) {
|
||||
return buildNaiveDiff(header, oldLines, newLines)
|
||||
}
|
||||
|
||||
const hunks = computeLineDiff(oldLines, newLines)
|
||||
if (hunks.length === 0) return null
|
||||
|
||||
const unifiedHunks = buildUnifiedHunks(oldLines, hunks, contextLines)
|
||||
|
||||
return `${header}\n${unifiedHunks}`
|
||||
}
|
||||
|
||||
function splitLines(text: string): string[] {
|
||||
const lines = text.split("\n")
|
||||
// Remove trailing empty line from trailing newline
|
||||
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
||||
lines.pop()
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
/**
|
||||
* Naive diff: all deletions first, then all additions, with a single hunk header.
|
||||
* Used as fallback when inputs are too large for LCS.
|
||||
*/
|
||||
function buildNaiveDiff(
|
||||
header: string,
|
||||
oldLines: string[],
|
||||
newLines: string[]
|
||||
): string {
|
||||
const oldStart = oldLines.length === 0 ? 0 : 1
|
||||
const newStart = newLines.length === 0 ? 0 : 1
|
||||
const hunkHeader = `@@ -${oldStart},${oldLines.length} +${newStart},${newLines.length} @@`
|
||||
|
||||
const parts = [header, hunkHeader]
|
||||
for (const line of oldLines) parts.push(`-${line}`)
|
||||
for (const line of newLines) parts.push(`+${line}`)
|
||||
return parts.join("\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert DiffHunk[] into unified diff text with context lines and hunk headers.
|
||||
*
|
||||
* Groups nearby hunks that overlap in their context windows into a single
|
||||
* unified hunk, producing output similar to `diff -u`.
|
||||
*/
|
||||
function buildUnifiedHunks(
|
||||
oldLines: string[],
|
||||
hunks: DiffHunk[],
|
||||
contextLines: number
|
||||
): string {
|
||||
// Build "change regions" with context, then merge overlapping ones
|
||||
const regions = hunks.map((hunk) => ({
|
||||
// Context-expanded range in old lines
|
||||
ctxOldStart: Math.max(0, hunk.baseStart - contextLines),
|
||||
ctxOldEnd: Math.min(
|
||||
oldLines.length,
|
||||
hunk.baseStart + hunk.baseCount + contextLines
|
||||
),
|
||||
hunk,
|
||||
}))
|
||||
|
||||
// Merge overlapping regions
|
||||
const merged: {
|
||||
ctxOldStart: number
|
||||
ctxOldEnd: number
|
||||
hunks: DiffHunk[]
|
||||
}[] = []
|
||||
|
||||
for (const region of regions) {
|
||||
const last = merged[merged.length - 1]
|
||||
if (last && region.ctxOldStart <= last.ctxOldEnd) {
|
||||
// Overlapping — extend and add hunk
|
||||
last.ctxOldEnd = Math.max(last.ctxOldEnd, region.ctxOldEnd)
|
||||
last.hunks.push(region.hunk)
|
||||
} else {
|
||||
merged.push({
|
||||
ctxOldStart: region.ctxOldStart,
|
||||
ctxOldEnd: region.ctxOldEnd,
|
||||
hunks: [region.hunk],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Render each merged region as a unified hunk
|
||||
const output: string[] = []
|
||||
|
||||
for (const group of merged) {
|
||||
const lines: string[] = []
|
||||
let oldCursor = group.ctxOldStart
|
||||
let newLineCount = 0
|
||||
const oldLineCount = group.ctxOldEnd - group.ctxOldStart
|
||||
|
||||
for (const hunk of group.hunks) {
|
||||
// Context lines before this change
|
||||
while (oldCursor < hunk.baseStart) {
|
||||
lines.push(` ${oldLines[oldCursor]}`)
|
||||
newLineCount++
|
||||
oldCursor++
|
||||
}
|
||||
|
||||
// Deleted lines
|
||||
for (let i = 0; i < hunk.baseCount; i++) {
|
||||
lines.push(`-${oldLines[hunk.baseStart + i]}`)
|
||||
oldCursor++
|
||||
}
|
||||
|
||||
// Added lines
|
||||
for (const newLine of hunk.newLines) {
|
||||
lines.push(`+${newLine}`)
|
||||
newLineCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Trailing context
|
||||
while (oldCursor < group.ctxOldEnd) {
|
||||
lines.push(` ${oldLines[oldCursor]}`)
|
||||
newLineCount++
|
||||
oldCursor++
|
||||
}
|
||||
|
||||
// Compute hunk header
|
||||
const oldStart = oldLineCount === 0 ? 0 : group.ctxOldStart + 1
|
||||
const newStart =
|
||||
newLineCount === 0
|
||||
? 0
|
||||
: group.ctxOldStart +
|
||||
1 +
|
||||
computeNewOffset(group.hunks, group.ctxOldStart)
|
||||
|
||||
output.push(
|
||||
`@@ -${oldStart},${oldLineCount} +${newStart},${newLineCount} @@`
|
||||
)
|
||||
output.push(...lines)
|
||||
}
|
||||
|
||||
return output.join("\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the offset applied to new-line numbering by hunks before a given position.
|
||||
*/
|
||||
function computeNewOffset(hunks: DiffHunk[], beforeOldLine: number): number {
|
||||
let offset = 0
|
||||
for (const hunk of hunks) {
|
||||
if (hunk.baseStart >= beforeOldLine) break
|
||||
offset += hunk.newLines.length - hunk.baseCount
|
||||
}
|
||||
return offset
|
||||
}
|
||||
Reference in New Issue
Block a user