mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 14:30:13 +00:00
0cc033edd8
POST /workspaces/{ws}/items/{ref}/patch {edits:[{old,new,line_hint?}],
expected_seq?} replaces exact text in the body. It runs through updateItem,
so it takes every PATCH guard; the body is computed inside the store
transaction from the row it replaces. Each old must occur exactly once;
edits apply in order, all or nothing; every patch writes a version. The
answer carries a unified diff of what changed.
expected_seq does not refuse: a moved seq is applied over when the
anchors still match, and answered rebased with the intervening writes,
read from a seq chain stamped into item activity entries ({prev_seq, seq,
version_id}). complete is decided at runtime, so any write that left no
stamp reads incomplete; a guard test classifies every seq-moving store
statement. Refusals write nothing: patch_anchor_missing,
patch_anchor_ambiguous, patch_room_open (an open editor; step 2).
CLI: pad item patch, pad item show --lines. MCP 0.87 (additive):
pad_item.patch, get lines, three allow-listed codes. The timeline treats
the chain and patch keys as bookkeeping, not a change.
Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
69 lines
1.9 KiB
Go
69 lines
1.9 KiB
Go
package bodypatch
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// BodyLines is a numbered slice of an item's body (PLAN-3578), for targeting
|
|
// and quoting a patch. Line numbers are 1-based over the body as stored,
|
|
// split on "\n". They are for reading only: a patch anchors on text.
|
|
type BodyLines struct {
|
|
Ref string `json:"ref"`
|
|
Seq int64 `json:"seq"`
|
|
ContentState string `json:"content_state,omitempty"`
|
|
Start int `json:"start"`
|
|
End int `json:"end"`
|
|
TotalLines int `json:"total_lines"`
|
|
Lines []BodyLine `json:"lines"`
|
|
}
|
|
|
|
// BodyLine is one numbered line.
|
|
type BodyLine struct {
|
|
N int `json:"n"`
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
// ParseLineRange reads "A-B", "A-" (to the end) or "A". A range past the
|
|
// body's end is clipped by SliceBodyLines, not refused here.
|
|
func ParseLineRange(s string) (start, end int, err error) {
|
|
bad := fmt.Errorf("lines wants A-B, A- or A (1-based), got %q", s)
|
|
a, b, isRange := strings.Cut(strings.TrimSpace(s), "-")
|
|
start, err = strconv.Atoi(a)
|
|
if err != nil || start < 1 {
|
|
return 0, 0, bad
|
|
}
|
|
switch {
|
|
case !isRange:
|
|
return start, start, nil
|
|
case b == "":
|
|
return start, 0, nil // 0: to the end
|
|
}
|
|
end, err = strconv.Atoi(b)
|
|
if err != nil || end < start {
|
|
return 0, 0, bad
|
|
}
|
|
return start, end, nil
|
|
}
|
|
|
|
// SliceBodyLines numbers content's lines start..end (end 0 = to the end),
|
|
// clipped to the body. A body that ends in "\n" has no empty last line.
|
|
func SliceBodyLines(content string, start, end int) (lines []BodyLine, from, to, total int) {
|
|
all := strings.Split(strings.TrimSuffix(content, "\n"), "\n")
|
|
if content == "" {
|
|
all = nil
|
|
}
|
|
total = len(all)
|
|
if end == 0 || end > total {
|
|
end = total
|
|
}
|
|
for n := start; n <= end; n++ {
|
|
lines = append(lines, BodyLine{N: n, Text: all[n-1]})
|
|
}
|
|
if len(lines) == 0 {
|
|
lines = []BodyLine{}
|
|
}
|
|
return lines, start, end, total
|
|
}
|