semantic_code_edit_mcp/
selector.rs

1use std::fmt::Display;
2
3// Simplified text-based selector system
4use anyhow::Result;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Copy)]
9pub enum Operation {
10    #[serde(rename = "insert_before")]
11    InsertBefore,
12    #[serde(rename = "insert_after")]
13    InsertAfter,
14    #[serde(rename = "insert_after_node")]
15    InsertAfterNode,
16    #[serde(rename = "replace_range")]
17    ReplaceRange,
18    #[serde(rename = "replace_exact")]
19    ReplaceExact,
20    #[serde(rename = "replace_node")]
21    ReplaceNode,
22}
23
24impl Operation {
25    pub fn as_str(&self) -> &'static str {
26        match self {
27            Operation::InsertBefore => "insert before",
28            Operation::InsertAfter => "insert after",
29            Operation::InsertAfterNode => "insert after node",
30            Operation::ReplaceRange => "replace range",
31            Operation::ReplaceExact => "replace exact",
32            Operation::ReplaceNode => "replace node",
33        }
34    }
35}
36impl Display for Operation {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        f.write_str(self.as_str())
39    }
40}
41
42#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
43pub struct Selector {
44    /// The type of edit operation to perform.
45    ///
46    /// Insert Operations
47    /// - **`insert_before`** - Insert content immediately before the anchor text
48    /// - **`insert_after`** - Insert content immediately after the anchor text  
49    /// - **`insert_after_node`** - Insert content after the complete AST node containing the anchor
50    ///
51    /// Replace Operations
52    /// - **`replace_exact`** - Replace only the exact anchor text
53    /// - **`replace_node`** - Replace the entire AST node containing the anchor
54    /// - **`replace_range`** - Replace everything from anchor to end (requires `end` field)
55    ///
56    /// ## Choosing the Right Operation
57    ///
58    /// **For adding new code:**
59    /// - Use `insert_before` or `insert_after` for precise placement
60    /// - Use `insert_after_node` when you want to add after a complete statement/declaration
61    ///
62    /// **For changing existing code:**
63    /// - Use `replace_exact` for small, precise text changes
64    /// - Use `replace_node` for changing entire functions, classes, blocks, or statements
65    /// - Use `replace_range` for changing multi-line sections with clear start/end boundaries
66    pub operation: Operation,
67
68    /// Text to locate in the source code as the target for the operation.
69    ///
70    /// Should be a short, distinctive piece of text that uniquely identifies the location.
71    /// For range operations, this marks the start of the range.
72    /// For node operations, this should cover the start of the ast node.
73    ///
74    /// Tips for Good Anchors
75    ///
76    /// - **Keep anchors short but unique** - "fn main" instead of the entire function signature
77    /// - **Use distinctive text** - function names, keywords, or unique comments work well
78    /// - **Avoid whitespace-only anchors** - they're often not unique enough
79    /// - **Test your anchor** - if it appears multiple times, the tool will find the best placement
80    ///
81    /// # Examples
82    /// - `"fn main() {"` - Targets a function definition
83    /// - `"struct User {"` - Targets a struct definition  
84    /// - `"// TODO: implement"` - Targets a specific comment
85    /// - `"import React"` - Targets an import statement
86    pub anchor: String,
87
88    /// End boundary for replace range operations only.
89    ///
90    /// When specified, defines the end of the text range to be replaced.
91    /// Use this to avoid repeating long blocks of content just to replace them.
92    ///
93    /// # Example
94    /// ```json
95    /// {
96    ///   "operation": "replace_range",
97    ///   "anchor": "// Start replacing here",
98    ///   "end": "// Stop replacing here"
99    /// }
100    /// ```
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub end: Option<String>,
103}
104
105impl Selector {
106    pub fn operation_name(&self) -> &str {
107        self.operation.as_str()
108    }
109
110    /// Validate that the selector is properly formed
111    pub fn validate(&self) -> Result<(), String> {
112        let Self {
113            operation,
114            anchor,
115            end,
116        } = self;
117
118        let mut errors = vec![];
119        if anchor.trim().is_empty() {
120            errors.push("- `anchor` cannot be empty");
121        }
122
123        match operation {
124            Operation::InsertBefore | Operation::InsertAfter | Operation::InsertAfterNode => {
125                if end.is_some() {
126                    errors.push(
127                        "- End is not relevant for insert operations. Did you mean to `replace`?",
128                    );
129                }
130            }
131            Operation::ReplaceRange => {
132                if end.is_none() {
133                    errors.push("- End is required for range replacement");
134                }
135            }
136            Operation::ReplaceExact | Operation::ReplaceNode => {
137                if end.is_some() {
138                    errors.push("- `end` is not relevant for `replace_exact` operations. Did you intend to `replace_range`?");
139                }
140            }
141        }
142
143        if errors.is_empty() {
144            Ok(())
145        } else {
146            Err(errors.join("\n"))
147        }
148    }
149}