mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 22:37:03 +00:00
99aac1687b
* test: pad library diff/update and the builtin_update capability (TASK-3462 U3c, red) * feat(cli): pad library diff and pad library update for built-in conventions and playbooks (TASK-3462 U3c) - diff shows what Pad's library changed since the item was made, what the user changed (or a plain comparison when that is unknown), and the settings an update would replace. - update takes the library's text, guarded by the seq read just before. It refuses content_pending_flush unless --overwrite-pending-edits, explains update_conflict, and reports a current item without sending. - The server advertises builtin_update; both commands refuse without it, since an older server's bare 404 reads like not_builtin. Not on MCP, per the design. * test: pad library diff/update codex r1 findings (TASK-3462 U3c, red) * fix(cli): pad library diff/update codex r1 findings (TASK-3462 U3c) - The JSON diff carries the current text from a server that does not send it. - A no-op under --format json is a JSON result. - A failed capabilities probe says so; it is no longer reported as an older server. - An update prints the write's warnings: applied to an open editor, and pending edits pruned. - content_pending_flush is rendered by the shared helper, which names set-aside rows apart from an open editor's; the flag's help covers both. - The diff names an end-of-file newline change. * test(cli): built-in diff keeps large numbers and refuses a mixed-version fallback (TASK-3462 U3c, codex r2, red) * fix(cli): built-in diff keeps numbers as written and refuses a fallback from another version (TASK-3462 U3c, codex r2) BuiltinText decodes its fields keeping numbers, and the fallback read of the item does the same, with status and nulls dropped to match the server's current. A fallback whose seq differs from the state's is refused instead of mixed in. * test(cli): built-in update reports undeclared fields (TASK-3462 U3c, codex r5, red) * fix(cli): built-in update reports undeclared fields like any item update (TASK-3462 U3c, codex r5)
148 lines
3.6 KiB
Go
148 lines
3.6 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/sergi/go-diff/diffmatchpatch"
|
|
|
|
"github.com/PerpetualSoftware/pad/internal/models"
|
|
)
|
|
|
|
// FormatLineDiff renders old -> new as lines prefixed "-", "+" or " ",
|
|
// keeping at most context unchanged lines around each change and eliding the
|
|
// rest with "…" (TASK-3462 U3c). Empty when the two are equal.
|
|
func FormatLineDiff(oldText, newText string, context int) string {
|
|
if oldText == newText {
|
|
return ""
|
|
}
|
|
// A change to the final newline alone renders as two identical-looking
|
|
// lines, so it is named (codex r1), as diff does.
|
|
oldNL, newNL := strings.HasSuffix(oldText, "\n"), strings.HasSuffix(newText, "\n")
|
|
dmp := diffmatchpatch.New()
|
|
a, b, lines := dmp.DiffLinesToChars(oldText, newText)
|
|
diffs := dmp.DiffCharsToLines(dmp.DiffMain(a, b, false), lines)
|
|
|
|
type line struct {
|
|
op diffmatchpatch.Operation
|
|
text string
|
|
}
|
|
var all []line
|
|
for _, d := range diffs {
|
|
text := strings.TrimSuffix(d.Text, "\n")
|
|
for _, l := range strings.Split(text, "\n") {
|
|
all = append(all, line{d.Type, l})
|
|
}
|
|
}
|
|
keep := make([]bool, len(all))
|
|
for i, l := range all {
|
|
if l.op == diffmatchpatch.DiffEqual {
|
|
continue
|
|
}
|
|
for j := i - context; j <= i+context; j++ {
|
|
if j >= 0 && j < len(all) {
|
|
keep[j] = true
|
|
}
|
|
}
|
|
}
|
|
var out strings.Builder
|
|
elided := false
|
|
for i, l := range all {
|
|
if !keep[i] {
|
|
if !elided {
|
|
out.WriteString(" …\n")
|
|
elided = true
|
|
}
|
|
continue
|
|
}
|
|
elided = false
|
|
switch l.op {
|
|
case diffmatchpatch.DiffInsert:
|
|
out.WriteString("+")
|
|
case diffmatchpatch.DiffDelete:
|
|
out.WriteString("-")
|
|
default:
|
|
out.WriteString(" ")
|
|
}
|
|
out.WriteString(l.text)
|
|
out.WriteString("\n")
|
|
}
|
|
if oldNL != newNL {
|
|
side := "new"
|
|
if !oldNL {
|
|
side = "old"
|
|
}
|
|
out.WriteString("\\ No newline at end of file (" + side + ")\n")
|
|
}
|
|
return out.String()
|
|
}
|
|
|
|
// BuiltinFieldChange is one setting an accepted update would replace.
|
|
type BuiltinFieldChange struct {
|
|
Key string
|
|
Current any
|
|
Library any
|
|
HasCur bool
|
|
HasLib bool
|
|
}
|
|
|
|
// BuiltinFieldChanges lists the settings taking the library's text would
|
|
// replace: every key the library writes whose value differs from the item's,
|
|
// and every key the seed had that the library dropped (the update removes
|
|
// those). status is never touched by an update, so it is never listed. Values
|
|
// compare as canonical JSON, so number spelling and key order do not count.
|
|
func BuiltinFieldChanges(current, library, seed map[string]any) []BuiltinFieldChange {
|
|
keys := map[string]bool{}
|
|
for k := range library {
|
|
keys[k] = true
|
|
}
|
|
for k := range seed {
|
|
if _, kept := library[k]; !kept {
|
|
keys[k] = true
|
|
}
|
|
}
|
|
delete(keys, "status")
|
|
sorted := make([]string, 0, len(keys))
|
|
for k := range keys {
|
|
sorted = append(sorted, k)
|
|
}
|
|
sort.Strings(sorted)
|
|
canon := func(v any) string {
|
|
b, err := json.Marshal(models.CanonicalJSONNumbers(v))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return string(b)
|
|
}
|
|
var out []BuiltinFieldChange
|
|
for _, k := range sorted {
|
|
cur, hasCur := current[k]
|
|
lib, hasLib := library[k]
|
|
if !hasCur && !hasLib {
|
|
continue
|
|
}
|
|
if hasCur && hasLib && canon(cur) == canon(lib) {
|
|
continue
|
|
}
|
|
out = append(out, BuiltinFieldChange{Key: k, Current: cur, Library: lib, HasCur: hasCur, HasLib: hasLib})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ShowFieldValue renders a setting's value for a terminal: a string as
|
|
// itself, anything else as JSON.
|
|
func ShowFieldValue(v any, present bool) string {
|
|
if !present {
|
|
return "(none)"
|
|
}
|
|
if s, ok := v.(string); ok {
|
|
return s
|
|
}
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return "?"
|
|
}
|
|
return string(b)
|
|
}
|