1use std::path::PathBuf;
5
6use super::{xmlish, DialectProvider};
7use crate::{
8 config::Config,
9 context::ContextProvider,
10 patch::{Change, Patch, Replace, Smart, UDiff, WriteFile},
11 session::{ModelResponse, Operation, Session},
12 Result, TenxError,
13};
14use fs_err as fs;
15
16const SYSTEM: &str = include_str!("./tags-system.txt");
17const SMART: &str = include_str!("./tags-smart.txt");
18const REPLACE: &str = include_str!("./tags-replace.txt");
19const UDIFF: &str = include_str!("./tags-udiff.txt");
20const EDIT: &str = include_str!("./tags-edit.txt");
21
22#[derive(Debug, Default, PartialEq, Eq, Clone)]
24pub struct Tags {
25 pub smart: bool,
26 pub replace: bool,
27 pub udiff: bool,
28 pub edit: bool,
29}
30
31impl Tags {
32 pub fn new(smart: bool, replace: bool, udiff: bool, edit: bool) -> Self {
33 Self {
34 smart,
35 replace,
36 udiff,
37 edit,
38 }
39 }
40}
41
42impl DialectProvider for Tags {
43 fn name(&self) -> &'static str {
44 "tags"
45 }
46
47 fn system(&self) -> String {
48 let mut out = SYSTEM.to_string();
49 if self.smart {
50 out.push_str(SMART);
51 }
52 if self.replace {
53 out.push_str(REPLACE);
54 }
55 if self.udiff {
56 out.push_str(UDIFF);
57 }
58 if self.edit {
59 out.push_str(EDIT);
60 }
61 out
62 }
63
64 fn render_context(&self, config: &Config, s: &Session) -> Result<String> {
65 if self.system().is_empty() {
66 return Ok("There is no non-editable context.".into());
67 }
68
69 let mut rendered = String::new();
70 rendered.push_str("<context>\n");
71 for cspec in s.contexts() {
72 for ctx in cspec.context_items(config, s)? {
73 let txt = format!(
74 "<item name=\"{}\" type=\"{:?}\">\n{}\n</item>\n",
75 ctx.source, ctx.ty, ctx.body
76 );
77 rendered.push_str(&txt)
78 }
79 }
80 rendered.push_str("</context>");
81 Ok(rendered)
82 }
83
84 fn render_editables(
85 &self,
86 config: &Config,
87 _session: &Session,
88 paths: Vec<PathBuf>,
89 ) -> Result<String> {
90 let mut rendered = String::new();
91 for path in paths {
92 let contents = fs::read_to_string(config.abspath(&path)?)?;
93 rendered.push_str(&format!(
94 "<editable path=\"{}\">\n{}</editable>\n\n",
95 path.display(),
96 contents
97 ));
98 }
99 Ok(rendered)
100 }
101
102 fn render_step_request(
103 &self,
104 _config: &Config,
105 session: &Session,
106 offset: usize,
107 ) -> Result<String> {
108 let prompt = session
109 .steps()
110 .get(offset)
111 .ok_or_else(|| TenxError::Internal("Invalid prompt offset".into()))?;
112 let mut rendered = String::new();
113 rendered.push_str(&format!("\n<prompt>\n{}\n</prompt>\n\n", &prompt.prompt));
114 Ok(rendered)
115 }
116
117 fn parse(&self, response: &str) -> Result<ModelResponse> {
141 let mut patch = Patch::default();
142 let mut operations = vec![];
143 let mut lines = response.lines().map(String::from).peekable();
144 let mut comment = None;
145
146 while let Some(line) = lines.peek() {
147 if let Some(tag) = xmlish::parse_open(line) {
148 match tag.name.as_str() {
149 "smart" => {
150 let path = tag
151 .attributes
152 .get("path")
153 .ok_or_else(|| TenxError::ResponseParse {
154 user: "Failed to parse model response".into(),
155 model: format!(
156 "Missing path attribute in smart tag. Line: '{}'",
157 line
158 ),
159 })?
160 .clone();
161 let (_, content) = xmlish::parse_block("smart", &mut lines)?;
162 patch.changes.push(Change::Smart(Smart {
163 path: path.into(),
164 text: content.join("\n"),
165 }));
166 }
167 "write_file" => {
168 let path = tag
169 .attributes
170 .get("path")
171 .ok_or_else(|| TenxError::ResponseParse {
172 user: "Failed to parse model response".into(),
173 model: format!(
174 "Missing path attribute in write_file tag. Line: '{}'",
175 line
176 ),
177 })?
178 .clone();
179 let (_, content) = xmlish::parse_block("write_file", &mut lines)?;
180 patch.changes.push(Change::Write(WriteFile {
181 path: path.into(),
182 content: content.join("\n"),
183 }));
184 }
185 "replace" => {
186 let path = tag
187 .attributes
188 .get("path")
189 .ok_or_else(|| TenxError::ResponseParse {
190 user: "Failed to parse model response".into(),
191 model: format!(
192 "Missing path attribute in replace tag. Line: '{}'",
193 line
194 ),
195 })?
196 .clone();
197 let (_, replace_content) = xmlish::parse_block("replace", &mut lines)?;
198 let mut replace_lines = replace_content.into_iter().peekable();
199 let (_, old) = xmlish::parse_block("old", &mut replace_lines)?;
200 let (_, new) = xmlish::parse_block("new", &mut replace_lines)?;
201 patch.changes.push(Change::Replace(Replace {
202 path: path.into(),
203 old: old.join("\n"),
204 new: new.join("\n"),
205 }));
206 }
207 "udiff" => {
208 let (_, content) = xmlish::parse_block("udiff", &mut lines)?;
209 patch
210 .changes
211 .push(Change::UDiff(UDiff::new(content.join("\n"))?));
212 }
213 "comment" => {
214 let (_, content) = xmlish::parse_block("comment", &mut lines)?;
215 comment = Some(content.join("\n"));
216 }
217 "edit" => {
218 let (_, content) = xmlish::parse_block("edit", &mut lines)?;
219 for line in content {
220 let path = line.trim().to_string();
221 if !path.is_empty() {
222 operations.push(Operation::Edit(PathBuf::from(path)));
223 }
224 }
225 }
226 _ => {
227 lines.next();
228 }
229 }
230 } else {
231 lines.next();
232 }
233 }
234 Ok(ModelResponse {
235 patch: Some(patch),
236 operations,
237 usage: None,
238 comment,
239 response_text: Some(response.to_string()),
240 })
241 }
242
243 fn render_step_response(
244 &self,
245 _config: &Config,
246 session: &Session,
247 offset: usize,
248 ) -> Result<String> {
249 let step = session
250 .steps()
251 .get(offset)
252 .ok_or_else(|| TenxError::Internal("Invalid step offset".into()))?;
253 if let Some(resp) = &step.model_response {
254 let mut rendered = String::new();
255 if let Some(comment) = &resp.comment {
256 rendered.push_str(&format!("<comment>\n{}\n</comment>\n\n", comment));
257 }
258 for op in &resp.operations {
259 let Operation::Edit(path) = op;
260 rendered.push_str(&format!("<edit>\n{}\n</edit>\n\n", path.display()));
261 }
262 if let Some(patch) = &resp.patch {
263 for change in &patch.changes {
264 match change {
265 Change::Write(write_file) => {
266 rendered.push_str(&format!(
267 "<write_file path=\"{}\">\n{}\n</write_file>\n\n",
268 write_file.path.display(),
269 write_file.content
270 ));
271 }
272 Change::Replace(replace) => {
273 rendered.push_str(&format!(
274 "<replace path=\"{}\">\n<old>\n{}\n</old>\n<new>\n{}\n</new>\n</replace>\n\n",
275 replace.path.display(),
276 replace.old,
277 replace.new
278 ));
279 }
280 Change::Smart(smart) => {
281 rendered.push_str(&format!(
282 "<smart path=\"{}\">\n{}\n</smart>\n\n",
283 smart.path.display(),
284 smart.text
285 ));
286 }
287 Change::UDiff(udiff) => {
288 rendered.push_str(&format!("<udiff>\n{}\n</udiff>\n\n", udiff.patch));
289 }
290 }
291 }
292 }
293 Ok(rendered)
294 } else {
295 Ok(String::new())
296 }
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use crate::session::{Step, StepType};
304
305 use indoc::indoc;
306 use pretty_assertions::assert_eq;
307
308 #[test]
309 fn test_parse_response_basic() {
310 let d = Tags {
311 smart: true,
312 replace: true,
313 udiff: false,
314 edit: false,
315 };
316
317 let input = indoc! {r#"
318 <comment>
319 This is a comment.
320 </comment>
321 <write_file path="/path/to/file2.txt">
322 This is the content of the file.
323 </write_file>
324 <replace path="/path/to/file.txt">
325 <old>
326 Old content
327 </old>
328 <new>
329 New content
330 </new>
331 </replace>
332 "#};
333
334 let expected = ModelResponse {
335 patch: Some(Patch {
336 changes: vec![
337 Change::Write(WriteFile {
338 path: PathBuf::from("/path/to/file2.txt"),
339 content: "This is the content of the file.".to_string(),
340 }),
341 Change::Replace(Replace {
342 path: PathBuf::from("/path/to/file.txt"),
343 old: "Old content".to_string(),
344 new: "New content".to_string(),
345 }),
346 ],
347 }),
348 operations: vec![],
349 usage: None,
350 comment: Some("This is a comment.".to_string()),
351 response_text: Some(input.to_string()),
352 };
353
354 let result = d.parse(input).unwrap();
355 assert_eq!(result, expected);
356 }
357
358 #[test]
359 fn test_parse_edit() {
360 let d = Tags::default();
361
362 let input = indoc! {r#"
363 <comment>
364 Testing edit tag
365 </comment>
366 <edit>
367 src/main.rs
368 </edit>
369 <edit>
370 with/leading/spaces.rs
371 </edit>
372 "#};
373
374 let result = d.parse(input).unwrap();
375 assert_eq!(
376 result.operations,
377 vec![
378 Operation::Edit(PathBuf::from("src/main.rs")),
379 Operation::Edit(PathBuf::from("with/leading/spaces.rs")),
380 ]
381 );
382 }
383
384 #[test]
385 fn test_render_edit() {
386 let d = Tags::default();
387 let mut session = Session::default();
388
389 let response = ModelResponse {
390 comment: Some("A comment".into()),
391 patch: None,
392 operations: vec![
393 Operation::Edit(PathBuf::from("src/main.rs")),
394 Operation::Edit(PathBuf::from("src/lib.rs")),
395 ],
396 usage: None,
397 response_text: Some("Test response".into()),
398 };
399
400 session.steps_mut().push(Step::new(
401 "test_model".into(),
402 "test".into(),
403 StepType::Code,
404 ));
405 if let Some(step) = session.steps_mut().last_mut() {
406 step.model_response = Some(response);
407 }
408
409 let result = d
410 .render_step_response(&Config::default(), &session, 0)
411 .unwrap();
412 assert_eq!(
413 result,
414 indoc! {r#"
415 <comment>
416 A comment
417 </comment>
418
419 <edit>
420 src/main.rs
421 </edit>
422
423 <edit>
424 src/lib.rs
425 </edit>
426
427 "#}
428 );
429 }
430
431 #[test]
432 fn test_parse_edit_multiline() {
433 let d = Tags::default();
434
435 let input = indoc! {r#"
436 <edit>
437 /path/to/first
438 /path/to/second
439 </edit>
440 "#};
441
442 let result = d.parse(input).unwrap();
443 assert_eq!(
444 result.operations,
445 vec![
446 Operation::Edit(PathBuf::from("/path/to/first")),
447 Operation::Edit(PathBuf::from("/path/to/second")),
448 ]
449 );
450 }
451
452 #[test]
453 fn test_render_system() {
454 let tags_with_smart = Tags {
455 smart: true,
456 replace: true,
457 udiff: false,
458 edit: false,
459 };
460 let tags_without_smart = Tags {
461 smart: false,
462 replace: true,
463 udiff: false,
464 edit: false,
465 };
466
467 let _system_with_smart = tags_with_smart.system();
469
470 let _system_without_smart = tags_without_smart.system();
472 }
473}