@@ -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<String>,
- expected_hunks: Vec<String>,
expected_patch: String,
expected_excerpts: Vec<ExpectedExcerpt>,
}
@@ -46,31 +50,19 @@ pub enum ExampleFormat {
impl NamedExample {
pub fn load(path: impl AsRef<Path>) -> Result<Self> {
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<HeadingLevel> = 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()