mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Repris d'AJEAN 0.16.0 (chat_diff.go et son test, repris tels quels : le fichier n'avait pas bougé chez nous depuis la 0.13.5). - Un fichier de 500 lignes écrit par l'agent affichait « +500 » pendant la frappe puis retombait à « +120 » : le diff envoyé à l'UI est plafonné à 120 lignes, et l'UI recomptait sur la version tronquée. lineDiff et addedDiff rendent désormais les VRAIS totaux, calculés avant la coupe, et ils voyagent dans le journal (added/removed) — donc survivent au refresh. Repli sur l'ancien décompte pour les conversations d'avant. - Une retouche d'une ligne dans un bloc de plus de 400 lignes s'affichait en remplacement complet (le LCS n'y était pas tenté). Les lignes communes en tête et en queue sont écartées d'abord : on voit le vrai changement, avec trois lignes de contexte, et il n'est plus repoussé hors de la fenêtre. - Un contenu terminé par un saut de ligne ne compte plus une ligne de trop, côté serveur comme dans la bulle en cours de frappe (bodyLineCount). Le vérificateur du mode Code (code_verify.go) journalise les mêmes champs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
181 lines
5.1 KiB
Go
181 lines
5.1 KiB
Go
package loki
|
|
|
|
// chat_diff.go — petit diff ligne à ligne pour l'interface : quand l'IA modifie
|
|
// un fichier (outil `edit`) ou une page de mémoire (`mem_edit`, `mem_add`), on
|
|
// envoie à l'UI le détail des lignes ajoutées et retirées, qu'elle affiche en
|
|
// vert (+) et rouge (-). Aucune dépendance : LCS classique sur les lignes, avec
|
|
// des garde-fous pour ne jamais transformer un gros remplacement en pavé.
|
|
//
|
|
// Les lignes envoyées sont bornées (diffMaxShown), mais les compteurs +N / -N
|
|
// sont calculés AVANT la coupe et renvoyés à part : l'UI les comptait dans le
|
|
// diff tronqué, d'où un « +500 » pendant l'écriture qui retombait à « +120 ».
|
|
|
|
import "strings"
|
|
|
|
const (
|
|
diffMaxLines = 400 // au-delà (partie modifiée), on compare sans détail (LCS trop lente)
|
|
diffMaxShown = 120 // lignes envoyées à l'UI (le reste est résumé)
|
|
diffContext = 3 // lignes identiques gardées autour d'un changement
|
|
diffOmitPrefix = "…"
|
|
)
|
|
|
|
// DiffLine est une ligne de diff : Op vaut " " (contexte), "-" ou "+".
|
|
type DiffLine struct {
|
|
Op string `json:"op"`
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
// lineDiff compare deux blocs de texte ligne à ligne et renvoie les lignes à
|
|
// afficher (bornées) avec le nombre RÉEL de lignes ajoutées et retirées.
|
|
//
|
|
// Les lignes communes en tête et en queue sont écartées avant la LCS : un
|
|
// changement d'une ligne dans un gros bloc reste un +1 -1 lisible (au lieu d'un
|
|
// « tout retiré, tout ajouté ») et le changement n'est pas repoussé hors des
|
|
// lignes affichées par des centaines de lignes de contexte.
|
|
func lineDiff(oldText, newText string) (lines []DiffLine, added, removed int) {
|
|
a := splitLines(oldText)
|
|
b := splitLines(newText)
|
|
pre := 0
|
|
for pre < len(a) && pre < len(b) && a[pre] == b[pre] {
|
|
pre++
|
|
}
|
|
suf := 0
|
|
for suf < len(a)-pre && suf < len(b)-pre && a[len(a)-1-suf] == b[len(b)-1-suf] {
|
|
suf++
|
|
}
|
|
ma, mb := a[pre:len(a)-suf], b[pre:len(b)-suf]
|
|
|
|
var mid []DiffLine
|
|
if len(ma) > diffMaxLines || len(mb) > diffMaxLines {
|
|
// Partie modifiée énorme : pas de LCS (coût quadratique), l'ancien en
|
|
// retrait puis le nouveau en ajout.
|
|
mid = make([]DiffLine, 0, len(ma)+len(mb))
|
|
for _, l := range ma {
|
|
mid = append(mid, DiffLine{Op: "-", Text: l})
|
|
}
|
|
for _, l := range mb {
|
|
mid = append(mid, DiffLine{Op: "+", Text: l})
|
|
}
|
|
} else {
|
|
mid = lcsDiff(ma, mb)
|
|
}
|
|
for _, d := range mid {
|
|
switch d.Op {
|
|
case "+":
|
|
added++
|
|
case "-":
|
|
removed++
|
|
}
|
|
}
|
|
|
|
// Contexte autour du changement : quelques lignes, le reste résumé.
|
|
var out []DiffLine
|
|
if pre > diffContext {
|
|
out = append(out, DiffLine{Op: " ", Text: omitted(pre - diffContext)})
|
|
}
|
|
for _, l := range a[max(0, pre-diffContext):pre] {
|
|
out = append(out, DiffLine{Op: " ", Text: l})
|
|
}
|
|
out = append(out, mid...)
|
|
for _, l := range a[len(a)-suf : len(a)-suf+min(suf, diffContext)] {
|
|
out = append(out, DiffLine{Op: " ", Text: l})
|
|
}
|
|
if suf > diffContext {
|
|
out = append(out, DiffLine{Op: " ", Text: omitted(suf - diffContext)})
|
|
}
|
|
return capLines(out), added, removed
|
|
}
|
|
|
|
// lcsDiff : diff exact par plus longue sous-séquence commune.
|
|
func lcsDiff(a, b []string) []DiffLine {
|
|
n, m := len(a), len(b)
|
|
lcs := make([][]int, n+1)
|
|
for i := range lcs {
|
|
lcs[i] = make([]int, m+1)
|
|
}
|
|
for i := n - 1; i >= 0; i-- {
|
|
for j := m - 1; j >= 0; j-- {
|
|
if a[i] == b[j] {
|
|
lcs[i][j] = lcs[i+1][j+1] + 1
|
|
} else if lcs[i+1][j] >= lcs[i][j+1] {
|
|
lcs[i][j] = lcs[i+1][j]
|
|
} else {
|
|
lcs[i][j] = lcs[i][j+1]
|
|
}
|
|
}
|
|
}
|
|
var out []DiffLine
|
|
i, j := 0, 0
|
|
for i < n && j < m {
|
|
switch {
|
|
case a[i] == b[j]:
|
|
out = append(out, DiffLine{Op: " ", Text: a[i]})
|
|
i++
|
|
j++
|
|
case lcs[i+1][j] >= lcs[i][j+1]:
|
|
out = append(out, DiffLine{Op: "-", Text: a[i]})
|
|
i++
|
|
default:
|
|
out = append(out, DiffLine{Op: "+", Text: b[j]})
|
|
j++
|
|
}
|
|
}
|
|
for ; i < n; i++ {
|
|
out = append(out, DiffLine{Op: "-", Text: a[i]})
|
|
}
|
|
for ; j < m; j++ {
|
|
out = append(out, DiffLine{Op: "+", Text: b[j]})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// addedDiff présente un contenu entièrement nouveau (création d'une page ou
|
|
// d'un fichier) : toutes ses lignes en ajout.
|
|
func addedDiff(text string) (lines []DiffLine, added int) {
|
|
ls := splitLines(text)
|
|
out := make([]DiffLine, 0, len(ls))
|
|
for _, l := range ls {
|
|
out = append(out, DiffLine{Op: "+", Text: l})
|
|
}
|
|
return capLines(out), len(ls)
|
|
}
|
|
|
|
func splitLines(s string) []string {
|
|
s = strings.ReplaceAll(s, "\r\n", "\n")
|
|
// Un saut de ligne final termine la dernière ligne, il n'en ouvre pas une vide.
|
|
s = strings.TrimSuffix(s, "\n")
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
return strings.Split(s, "\n")
|
|
}
|
|
|
|
func omitted(n int) string {
|
|
if n == 1 {
|
|
return diffOmitPrefix + "(1 ligne identique)"
|
|
}
|
|
return diffOmitPrefix + "(" + itoa(n) + " lignes identiques)"
|
|
}
|
|
|
|
// capLines borne la taille envoyée à l'UI et signale ce qui a été coupé.
|
|
func capLines(d []DiffLine) []DiffLine {
|
|
if len(d) <= diffMaxShown {
|
|
return d
|
|
}
|
|
cut := len(d) - diffMaxShown
|
|
out := append([]DiffLine{}, d[:diffMaxShown]...)
|
|
return append(out, DiffLine{Op: " ", Text: diffOmitPrefix + "(" + itoa(cut) + " lignes de plus)"})
|
|
}
|
|
|
|
func itoa(n int) string {
|
|
if n == 0 {
|
|
return "0"
|
|
}
|
|
var b []byte
|
|
for n > 0 {
|
|
b = append([]byte{byte('0' + n%10)}, b...)
|
|
n /= 10
|
|
}
|
|
return string(b)
|
|
}
|