roder-edit-core 0.1.0

Agentic software development tools and SDKs for Roder.
Documentation
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct EditHunk {
    pub id: Option<String>,
    pub path: String,
    pub old_start: u32,
    pub old_lines: u32,
    pub new_start: u32,
    pub new_lines: u32,
    pub diff: Vec<HunkDiffLine>,
    pub reverse_patch: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct HunkDiffLine {
    pub kind: HunkDiffLineKind,
    pub text: String,
    pub old_line: Option<u32>,
    pub new_line: Option<u32>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum HunkDiffLineKind {
    Context,
    Added,
    Removed,
}

pub fn text_edit_hunk(
    path: impl Into<String>,
    old_text: &str,
    new_text: &str,
    index: usize,
) -> EditHunk {
    let path = path.into();
    let old_lines = old_text.lines().map(str::to_string).collect::<Vec<_>>();
    let new_lines = new_text.lines().map(str::to_string).collect::<Vec<_>>();
    lines_hunk(path, old_lines, new_lines, index)
}

pub fn lines_hunk(
    path: impl Into<String>,
    old_lines: Vec<String>,
    new_lines: Vec<String>,
    index: usize,
) -> EditHunk {
    let path = path.into();
    let mut diff = Vec::new();
    for (old_line, line) in (1u32..).zip(old_lines.iter()) {
        diff.push(HunkDiffLine {
            kind: HunkDiffLineKind::Removed,
            text: line.clone(),
            old_line: Some(old_line),
            new_line: None,
        });
    }
    for (new_line, line) in (1u32..).zip(new_lines.iter()) {
        diff.push(HunkDiffLine {
            kind: HunkDiffLineKind::Added,
            text: line.clone(),
            old_line: None,
            new_line: Some(new_line),
        });
    }
    EditHunk {
        id: Some(format!("hunk-{}", index + 1)),
        path: path.clone(),
        old_start: 1,
        old_lines: old_lines.len() as u32,
        new_start: 1,
        new_lines: new_lines.len() as u32,
        diff,
        reverse_patch: Some(reverse_codex_patch(&path, &old_lines, &new_lines)),
    }
}

pub fn reverse_codex_patch(path: &str, old_lines: &[String], new_lines: &[String]) -> String {
    let mut patch = String::from("*** Begin Patch\n");
    patch.push_str(&format!("*** Update File: {path}\n@@\n"));
    for line in new_lines {
        patch.push('-');
        patch.push_str(line);
        patch.push('\n');
    }
    for line in old_lines {
        patch.push('+');
        patch.push_str(line);
        patch.push('\n');
    }
    patch.push_str("*** End Patch\n");
    patch
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn hunk_contains_reverse_patch() {
        let hunk = text_edit_hunk("src/lib.rs", "old", "new", 0);
        assert_eq!(hunk.old_lines, 1);
        assert_eq!(hunk.new_lines, 1);
        assert!(hunk.reverse_patch.unwrap().contains("-new\n+old"));
    }
}