semantic-edit-mcp 0.2.1

MCP server for semantic code editing with tree-sitter
use super::{EditPosition, Editor};
use crate::searcher::find_positions;
use fieldwork::Fieldwork;
use ropey::Rope;
use std::{
    borrow::Cow,
    fmt::{self, Debug, Formatter},
};
use tree_sitter::{InputEdit, Node, Point, Tree};

#[derive(Clone, Fieldwork)]
#[fieldwork(option_set_some)]
pub struct Edit<'editor, 'language> {
    editor: &'editor Editor<'language>,
    tree: Tree,
    rope: Rope,
    #[field(get, set, with, get_mut(deref = false), into)]
    content: Cow<'editor, str>,
    #[field(get, get_mut)]
    position: EditPosition,
    #[field(get = is_valid)]
    valid: Option<bool>,
    #[field(get, take)]
    message: Option<String>,
    #[field(get, take)]
    output: Option<String>,
    #[field(get, set, with, take)]
    nodes: Option<Vec<Node<'editor>>>,
    #[field(with, get, set)]
    annotation: Option<&'static str>,
}

impl PartialEq for Edit<'_, '_> {
    fn eq(&self, other: &Edit<'_, '_>) -> bool {
        std::ptr::eq(self.editor, other.editor)
            && self.content == other.content
            && self.position == other.position
            && self.nodes == other.nodes
    }
}

impl Eq for Edit<'_, '_> {}

impl<'editor, 'language> Debug for Edit<'editor, 'language> {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let alternate = f.alternate();
        let mut s = f.debug_struct("Edit");
        s.field("content", &self.content)
            .field("anchor", &self.editor.selector.anchor)
            .field("operation", &self.editor.selector.operation);

        if let Some(valid) = self.valid {
            s.field("valid", &valid);
        }
        if let Some(nodes) = &self.nodes {
            s.field(
                "node_kinds",
                &nodes
                    .iter()
                    .map(|node| node.kind().to_string())
                    .collect::<Vec<_>>()
                    .join(","),
            );
        }

        if let Some(edit_region) = self.edit_region() {
            s.field("edit_region", &edit_region);
        } else {
            let source_code = self.source_code();
            let start_byte = self.position.start_byte;
            let min = source_code[..start_byte]
                .rmatch_indices('\n')
                .nth(2)
                .map(|(n, _)| n)
                .unwrap_or(0);
            let max = source_code[start_byte..]
                .find('\n')
                .map(|n| n + start_byte)
                .unwrap_or(source_code.len());
            s.field(
                "insertion_point",
                &format!(
                    "{}<|>{}",
                    &source_code[min..start_byte],
                    &source_code[start_byte..max]
                ),
            );
        }
        if let Some(annotation) = self.annotation {
            s.field("annotation", &format_args!("{annotation}"));
        }

        if let Some(message) = self.message() {
            if alternate {
                s.field("message", &format_args!("{message}"));
            }
        }

        s.finish()
    }
}

impl<'editor, 'language> Edit<'editor, 'language> {
    pub fn new(editor: &'editor Editor<'language>, position: EditPosition) -> Self {
        Self {
            editor,
            tree: editor.tree.clone(),
            rope: editor.rope.clone(),
            position,
            content: Cow::Borrowed(&editor.content),
            valid: None,
            message: None,
            output: None,
            nodes: None,
            annotation: None,
        }
    }

    pub fn insert_before(mut self) -> Self {
        if let Some(edit_region) = self.edit_region() {
            if let Ok(positions) = find_positions(&self.content, edit_region) {
                if let Some((start, _)) = positions.last() {
                    match &mut self.content {
                        Cow::Borrowed(borrowed) => *borrowed = &borrowed[..*start],
                        Cow::Owned(owned) => *owned = owned[..*start].to_string(),
                    };
                }
            }
        }

        self.position.end_byte = None;
        self
    }

    pub fn insert_after(mut self) -> Option<Self> {
        if let Some(edit_region) = self.edit_region() {
            if let Ok(positions) = find_positions(&self.content, edit_region) {
                if let Some((_, end)) = positions.first() {
                    match &mut self.content {
                        Cow::Borrowed(borrowed) => *borrowed = &borrowed[*end..],
                        Cow::Owned(owned) => *owned = owned[*end..].to_string(),
                    };
                }
            }
        }

        self.position.start_byte = self.position.end_byte.take()?;
        Some(self)
    }

    pub fn edit_region(&self) -> Option<&'editor str> {
        if let EditPosition {
            start_byte,
            end_byte: Some(end_byte),
        } = self.position
        {
            self.editor.source_code.get(start_byte..end_byte)
        } else {
            None
        }
    }

    pub fn with_end_byte(mut self, end_byte: usize) -> Self {
        self.position.end_byte = Some(end_byte);
        self
    }

    fn byte_to_point(&self, byte_idx: usize) -> Point {
        let line = self.rope.byte_to_line(byte_idx);
        let line_start_byte = self.rope.line_to_byte(line);
        let column = byte_idx - line_start_byte;

        Point { row: line, column }
    }

    pub(crate) fn apply(&mut self) -> bool {
        if let Some(valid) = self.valid {
            return valid;
        }

        let content = &self.content;

        let EditPosition {
            start_byte,
            end_byte,
        } = self.position;

        let start_char = self.rope.byte_to_char(start_byte);
        let start_position = self.byte_to_point(start_byte);

        let (old_end_byte, old_end_position) = if let Some(old_end_byte) = end_byte {
            let end_char = self.rope.byte_to_char(old_end_byte);
            let old_end_position = self.byte_to_point(old_end_byte);

            self.rope.remove(start_char..end_char);

            (old_end_byte, old_end_position)
        } else {
            (start_byte, start_position)
        };

        self.rope.insert(start_char, content);

        let new_end_byte = start_byte + content.len();
        let new_end_position = self.byte_to_point(new_end_byte);

        self.tree.edit(&InputEdit {
            start_byte,
            old_end_byte,
            new_end_byte,
            start_position,
            old_end_position,
            new_end_position,
        });

        let output = self.rope.to_string();

        if let Some(tree) = self.editor.parse(&output, Some(&self.tree)) {
            self.tree = tree;
        } else {
            self.valid = Some(false);
            self.message = Some("Unable to parse result so no changes were made. The file is still in a good state. Try a different edit".into());
            return false;
        }

        let valid = if let Some(message) = self.validate(&output) {
            self.message = Some(message);
            false
        } else {
            self.message = Some(format!(
                "Applied {} operation",
                self.editor.selector.operation_name()
            ));

            match self.editor.format_code(&output) {
                Ok(formatted) => {
                    self.output = Some(formatted);
                    true
                }
                Err(err) => {
                    self.message = Some(err);
                    false
                }
            }
        };

        self.valid = Some(valid);
        valid
    }

    fn validate(&mut self, output: &str) -> Option<String> {
        let errors = self.editor.validate_tree(&self.tree, output)?;
        let diff = self.editor.diff(output);
        Some(format!(
            "This edit would result in invalid syntax, but the file is still in a valid state. \
No change was performed.
Suggestion: Try a different change.\n
{errors}\n\n{diff}"
        ))
    }

    pub(crate) fn source_code(&self) -> &'editor str {
        self.editor.source_code()
    }

    pub(crate) fn start_byte(&self) -> usize {
        self.position.start_byte
    }

    pub(crate) fn set_start_byte(&mut self, start_byte: usize) -> &mut Self {
        self.position.start_byte = start_byte;
        self
    }

    pub(crate) fn modify(mut fun: impl FnMut(&mut Self)) -> impl FnMut(Self) -> Self {
        move |mut edit| {
            fun(&mut edit);
            edit
        }
    }

    pub(crate) fn with_start_byte(mut self, start_byte: usize) -> Self {
        self.position.start_byte = start_byte;
        self
    }
}