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}