返回 DeepSeek-Reasonix
preview.go
根目录 / internal / tool / builtin / preview.go
1 package builtin
2
3 import (
4 "encoding/json"
5 "fmt"
6 "os"
7
8 "reasonix/internal/diff"
9 fileenc "reasonix/internal/fileutil/encoding"
10 )
11
12 // preview.go gives the file-writing built-ins the optional tool.Previewer
13 // capability: compute the change a call would make, reading the current file
14 // but never writing. A front-end (e.g. a desktop approval card) calls Preview
15 // before the permission gate runs Execute.
16 //
17 // Each Preview mirrors its Execute's transformation exactly — same arg parsing,
18 // same uniqueness / not-found rules — so the previewed NewText equals what
19 // Execute would persist. That equality is asserted by TestPreviewMatchesExecute
20 // in preview_test.go, which runs Execute against a temp file and compares; if
21 // an Execute body ever drifts, that test fails rather than the preview lying.
22
23 // Preview computes the change write_file would make. A path that does not yet
24 // exist is a Create; an existing one is a Modify.
25 func (w writeFile) Preview(args json.RawMessage) (diff.Change, error) {
26 var p struct {
27 Path string `json:"path"`
28 Content string `json:"content"`
29 }
30 if err := json.Unmarshal(args, &p); err != nil {
31 return diff.Change{}, fmt.Errorf("invalid args: %w", err)
32 }
33 if p.Path == "" {
34 return diff.Change{}, fmt.Errorf("path is required")
35 }
36 p.Path = resolveIn(w.workDir, p.Path)
37
38 old, kind := "", diff.Create
39 if data, err := os.ReadFile(p.Path); err == nil {
40 enc, _ := fileenc.Detect(data)
41 old, kind = string(fileenc.Decode(data, enc)), diff.Modify
42 } else if !os.IsNotExist(err) {
43 return diff.Change{}, fmt.Errorf("read %s: %w", p.Path, err)
44 }
45 return diff.Build(p.Path, old, p.Content, kind), nil
46 }
47
48 // Preview computes the change edit_file would make. It enforces the same
49 // "old_string must occur exactly once" rule as Execute, returning that error
50 // when it doesn't — so a preview never shows a change the call couldn't make.
51 func (e editFile) Preview(args json.RawMessage) (diff.Change, error) {
52 var p struct {
53 Path string `json:"path"`
54 OldString string `json:"old_string"`
55 NewString string `json:"new_string"`
56 }
57 if err := json.Unmarshal(args, &p); err != nil {
58 return diff.Change{}, fmt.Errorf("invalid args: %w", err)
59 }
60 if p.Path == "" {
61 return diff.Change{}, fmt.Errorf("path is required")
62 }
63 if p.OldString == "" {
64 return diff.Change{}, fmt.Errorf("old_string is required")
65 }
66 p.Path = resolveIn(e.workDir, p.Path)
67
68 content, _, err := readFileEncoded(p.Path)
69 if err != nil {
70 return diff.Change{}, fmt.Errorf("read %s: %w", p.Path, err)
71 }
72
73 applied := applyOldStringEdit(content, p.OldString, p.NewString, false)
74 switch {
75 case applied.applied == 1:
76 // ok
77 case applied.matches == 0:
78 return diff.Change{}, oldStringNotFoundError(p.Path, p.OldString, content)
79 default:
80 return diff.Change{}, oldStringNotUniqueError(p.Path, p.OldString, content, applied.matches, false)
81 }
82
83 return diff.Build(p.Path, content, applied.updated, diff.Modify), nil
84 }
85
86 // Preview computes the change multi_edit would make by replaying every edit
87 // against an in-memory buffer — exactly as Execute does — and diffing the
88 // result against the original. Any edit error surfaces here too, so a preview
89 // of an invalid batch fails the same way the call would.
90 func (m multiEdit) Preview(args json.RawMessage) (diff.Change, error) {
91 var p struct {
92 Path string `json:"path"`
93 Edits []editStep `json:"edits"`
94 }
95 if err := json.Unmarshal(args, &p); err != nil {
96 return diff.Change{}, fmt.Errorf("invalid args: %w", err)
97 }
98 if p.Path == "" {
99 return diff.Change{}, fmt.Errorf("path is required")
100 }
101 if len(p.Edits) == 0 {
102 return diff.Change{}, fmt.Errorf("edits must not be empty")
103 }
104 p.Path = resolveIn(m.workDir, p.Path)
105
106 content, _, err := readFileEncoded(p.Path)
107 if err != nil {
108 return diff.Change{}, fmt.Errorf("read %s: %w", p.Path, err)
109 }
110 original := content
111
112 for i, step := range p.Edits {
113 if step.OldString == "" {
114 return diff.Change{}, fmt.Errorf("edit %d: old_string is required", i+1)
115 }
116 result := applyOldStringEdit(content, step.OldString, step.NewString, step.ReplaceAll)
117 switch {
118 case result.applied > 0:
119 content = result.updated
120 case result.matches == 0:
121 return diff.Change{}, fmt.Errorf("edit %d: %w", i+1, oldStringNotFoundError(p.Path, step.OldString, content))
122 default:
123 return diff.Change{}, fmt.Errorf("edit %d: %w", i+1, oldStringNotUniqueError(p.Path, step.OldString, content, result.matches, true))
124 }
125 }
126 return diff.Build(p.Path, original, content, diff.Modify), nil
127 }
128
128 lines GO