diff --git a/crates/cloud_zeta2_prompt/src/cloud_zeta2_prompt.rs b/crates/cloud_zeta2_prompt/src/cloud_zeta2_prompt.rs index a0df39b50eb6753397f5afd37aa30b71b853b9c5..6caf9941845146dc0c30c4606f677e5ec816c137 100644 --- a/crates/cloud_zeta2_prompt/src/cloud_zeta2_prompt.rs +++ b/crates/cloud_zeta2_prompt/src/cloud_zeta2_prompt.rs @@ -212,7 +212,7 @@ pub fn write_codeblock<'a>( include_line_numbers: bool, output: &'a mut String, ) { - writeln!(output, "`````path={}", path.display()).unwrap(); + writeln!(output, "`````{}", path.display()).unwrap(); write_excerpts( excerpts, sorted_insertions, diff --git a/crates/zeta2/src/related_excerpts.rs b/crates/zeta2/src/related_excerpts.rs index dd27992274ae2b25ec07e2a47dc8a60b46f5f3f2..44388251e32678ff8d1b3ce594ab35996b235759 100644 --- a/crates/zeta2/src/related_excerpts.rs +++ b/crates/zeta2/src/related_excerpts.rs @@ -64,7 +64,7 @@ const SEARCH_PROMPT: &str = indoc! {r#" ## Current cursor context - `````path={current_file_path} + `````{current_file_path} {cursor_excerpt} ````` diff --git a/crates/zeta_cli/src/example.rs b/crates/zeta_cli/src/example.rs index a015c75474eb73557d24032b8f67261e44308f27..f127a3ceff99b1b45a7a0b91573d321e82186c87 100644 --- a/crates/zeta_cli/src/example.rs +++ b/crates/zeta_cli/src/example.rs @@ -1,19 +1,22 @@ use std::{ fmt::{self, Display}, - fs::File, - io::{Read, Write}, + io::Write, + mem, os::unix::ffi::OsStrExt, path::{Path, PathBuf}, }; use anyhow::Result; use clap::ValueEnum; +use pulldown_cmark::CowStr; use serde::{Deserialize, Serialize}; +const CURSOR_POSITION_HEADING: &str = "Cursor Position"; const EDIT_HISTORY_HEADING: &str = "Edit History"; -const EXPECTED_HUNKS_HEADING: &str = "Expected Hunks"; const EXPECTED_PATCH_HEADING: &str = "Expected Patch"; const EXPECTED_EXCERPTS_HEADING: &str = "Expected Excerpts"; +const REPOSITORY_URL_FIELD: &str = "repository_url"; +const REVISION_FIELD: &str = "revision"; pub struct NamedExample { name: String, @@ -24,8 +27,9 @@ pub struct NamedExample { pub struct Example { repository_url: String, commit: String, + cursor_path: PathBuf, + cursor_position: String, edit_history: Vec, - expected_hunks: Vec, expected_patch: String, expected_excerpts: Vec, } @@ -46,31 +50,19 @@ pub enum ExampleFormat { impl NamedExample { pub fn load(path: impl AsRef) -> Result { let path = path.as_ref(); - let mut file = File::open(path)?; + let content = std::fs::read_to_string(path)?; let ext = path.extension(); match ext.map(|s| s.as_bytes()) { - Some(b"json") => { - let mut content = Vec::new(); - file.read_to_end(&mut content)?; - Ok(Self { - name: path.file_name().unwrap_or_default().display().to_string(), - example: serde_json::from_slice(&content)?, - }) - } - Some(b"toml") => { - let mut content = String::new(); - file.read_to_string(&mut content)?; - Ok(Self { - name: path.file_name().unwrap_or_default().display().to_string(), - example: toml::from_str(&content)?, - }) - } - Some(b"md") => { - let mut content = String::new(); - file.read_to_string(&mut content)?; - Self::parse_md(&content) - } + Some(b"json") => Ok(Self { + name: path.file_name().unwrap_or_default().display().to_string(), + example: serde_json::from_str(&content)?, + }), + Some(b"toml") => Ok(Self { + name: path.file_name().unwrap_or_default().display().to_string(), + example: toml::from_str(&content)?, + }), + Some(b"md") => Self::parse_md(&content), Some(_) => { anyhow::bail!("Unrecognized example extension: {}", ext.unwrap().display()); } @@ -87,119 +79,104 @@ impl NamedExample { let parser = Parser::new(input); - let mut name = String::new(); - let mut repository_url = String::new(); - let mut commit = String::new(); - let mut edit_history = Vec::new(); - let mut expected_hunks = Vec::new(); - let mut expected_patch = String::new(); - let mut expected_excerpts = Vec::new(); + let mut named = NamedExample { + name: String::new(), + example: Example { + repository_url: String::new(), + commit: String::new(), + cursor_path: PathBuf::new(), + cursor_position: String::new(), + edit_history: Vec::new(), + expected_patch: String::new(), + expected_excerpts: Vec::new(), + }, + }; - let mut current_heading_level: Option = None; - let mut current_heading_text = String::new(); + let mut text = String::new(); let mut current_section = String::new(); - let mut in_code_block = false; - let mut current_code_block = String::new(); - let mut current_code_info = String::new(); + let mut block_info: CowStr = "".into(); for event in parser { match event { - Event::Start(Tag::Heading { level, .. }) => { - current_heading_level = Some(level); - current_heading_text.clear(); - } - Event::End(TagEnd::Heading(_)) => { - let heading_text = current_heading_text.trim(); - if let Some(HeadingLevel::H1) = current_heading_level { - if !name.is_empty() { - anyhow::bail!( - "Found multiple H1 headings. There should only be one with the name of the example." - ); + Event::Text(line) => { + text.push_str(&line); + + if !named.name.is_empty() + && current_section.is_empty() + // in h1 section + && let Some((field, value)) = line.split_once('=') + { + match field { + REPOSITORY_URL_FIELD => { + named.example.repository_url = value.to_string(); + } + REVISION_FIELD => { + named.example.commit = value.to_string(); + } + _ => { + eprintln!("Warning: Unrecognized field `{field}`"); + } } - name = heading_text.to_string(); - } else if let Some(HeadingLevel::H2) = current_heading_level { - current_section = heading_text.to_string(); } - current_heading_level = None; } - Event::Start(Tag::CodeBlock(kind)) => { - in_code_block = true; - current_code_block.clear(); - current_code_info = match kind { - CodeBlockKind::Fenced(info) => info.to_string(), - CodeBlockKind::Indented => String::new(), - }; + Event::End(TagEnd::Heading(HeadingLevel::H1)) => { + if !named.name.is_empty() { + anyhow::bail!( + "Found multiple H1 headings. There should only be one with the name of the example." + ); + } + named.name = mem::take(&mut text); } - Event::End(TagEnd::CodeBlock) => { - in_code_block = false; - - match current_section.as_str() { - EDIT_HISTORY_HEADING => { - edit_history.push(current_code_block.clone()); - } - EXPECTED_HUNKS_HEADING => { - expected_hunks.push(current_code_block.clone()); - } - EXPECTED_PATCH_HEADING => { - expected_patch = current_code_block.clone(); + Event::End(TagEnd::Heading(HeadingLevel::H2)) => { + current_section = mem::take(&mut text); + } + Event::End(TagEnd::Heading(level)) => { + anyhow::bail!("Unexpected heading level: {level}"); + } + Event::Start(Tag::CodeBlock(kind)) => { + match kind { + CodeBlockKind::Fenced(info) => { + block_info = info; } - EXPECTED_EXCERPTS_HEADING => { - if let Some(path_start) = current_code_info.find("path=") { - let path_str = ¤t_code_info[path_start + 5..]; - let path = PathBuf::from(path_str.trim()); - expected_excerpts.push(ExpectedExcerpt { - path, - text: current_code_block.clone(), - }); - } + CodeBlockKind::Indented => { + anyhow::bail!("Unexpected indented codeblock"); } - _ => {} - } + }; } - Event::Text(text) => { - if let Some(_) = current_heading_level { - current_heading_text.push_str(&text); - } else if in_code_block { - current_code_block.push_str(&text); - } else if current_section.is_empty() - && let Some(eq_pos) = text.find('=') - { - let key = text[..eq_pos].trim(); - let value = text[eq_pos + 1..].trim(); - match key { - "repository_url" => repository_url = value.to_string(), - "commit" => commit = value.to_string(), - _ => {} - } + Event::Start(_) => { + text.clear(); + block_info = "".into(); + } + Event::End(TagEnd::CodeBlock) => { + if current_section.eq_ignore_ascii_case(EDIT_HISTORY_HEADING) { + named.example.edit_history.push(mem::take(&mut text)); + } else if current_section.eq_ignore_ascii_case(CURSOR_POSITION_HEADING) { + let path = PathBuf::from(block_info.trim()); + named.example.cursor_path = path; + named.example.cursor_position = mem::take(&mut text); + } else if current_section.eq_ignore_ascii_case(EXPECTED_PATCH_HEADING) { + named.example.expected_patch = mem::take(&mut text); + } else if current_section.eq_ignore_ascii_case(EXPECTED_EXCERPTS_HEADING) { + let path = PathBuf::from(block_info.trim()); + named.example.expected_excerpts.push(ExpectedExcerpt { + path, + text: mem::take(&mut text), + }); + } else { + eprintln!("Warning: Unrecognized section `{current_section:?}`") } } _ => {} } } - if name.is_empty() { - anyhow::bail!("Missing required H1 heading for example name"); + if named.example.cursor_path.as_path() == Path::new("") + || named.example.cursor_position.is_empty() + { + anyhow::bail!("Missing cursor position codeblock"); } - if repository_url.is_empty() { - anyhow::bail!("Missing required field: repository_url"); - } - - if commit.is_empty() { - anyhow::bail!("Missing required field: commit"); - } - - Ok(Self { - name, - example: Example { - repository_url, - commit, - edit_history, - expected_hunks, - expected_patch, - expected_excerpts, - }, - }) + Ok(named) } pub fn write(&self, format: ExampleFormat, mut out: impl Write) -> Result<()> { @@ -216,8 +193,19 @@ impl NamedExample { impl Display for NamedExample { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "# {}\n\n", self.name)?; - write!(f, "repository_url = {}\n", self.example.repository_url)?; - write!(f, "commit = {}\n\n", self.example.commit)?; + write!( + f, + "{REPOSITORY_URL_FIELD} = {}\n", + self.example.repository_url + )?; + write!(f, "{REVISION_FIELD} = {}\n\n", self.example.commit)?; + + write!( + f, + "## {CURSOR_POSITION_HEADING}\n\n`````{}\n{}`````\n", + self.example.cursor_path.display(), + self.example.cursor_position + )?; write!(f, "## {EDIT_HISTORY_HEADING}\n\n")?; if !self.example.edit_history.is_empty() { @@ -228,14 +216,6 @@ impl Display for NamedExample { write!(f, "`````\n")?; } - if !self.example.expected_hunks.is_empty() { - write!(f, "\n## {EXPECTED_HUNKS_HEADING}\n\n`````diff\n")?; - for hunk in &self.example.expected_hunks { - write!(f, "{hunk}")?; - } - write!(f, "`````\n")?; - } - if !self.example.expected_patch.is_empty() { write!( f, @@ -250,7 +230,7 @@ impl Display for NamedExample { for excerpt in &self.example.expected_excerpts { write!( f, - "`````{}path={}\n{}`````\n\n", + "`````{}{}\n{}`````\n\n", excerpt .path .extension()