1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use thiserror::Error;
4
5use crate::ToolSpec;
6use crate::error::{ToolExecutionError, WorkspaceOpError};
7use crate::result::{EditResult, MultiEditResult};
8pub use steer_workspace::EditMatchPreview;
9use steer_workspace::error::non_unique_match_preview_suffix;
10
11#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
12#[serde(rename_all = "snake_case")]
13pub enum EditMatchMode {
14 ExactlyOne,
15 First,
16 All,
17 Nth,
18}
19
20pub const EDIT_TOOL_NAME: &str = "edit_file";
21
22pub struct EditToolSpec;
23
24impl ToolSpec for EditToolSpec {
25 type Params = EditParams;
26 type Result = EditResult;
27 type Error = EditError;
28
29 const NAME: &'static str = EDIT_TOOL_NAME;
30 const DISPLAY_NAME: &'static str = "Edit File";
31
32 fn execution_error(error: Self::Error) -> ToolExecutionError {
33 ToolExecutionError::Edit(error)
34 }
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Error)]
38#[serde(tag = "code", content = "details", rename_all = "snake_case")]
39pub enum EditFailure {
40 #[error("file not found: {file_path}")]
41 FileNotFound { file_path: String },
42
43 #[error(
44 "edit #{edit_index} has an empty old_string; use write_file to create or overwrite files"
45 )]
46 EmptyOldString { edit_index: usize },
47
48 #[error("string not found for edit #{edit_index} in file {file_path}")]
49 StringNotFound {
50 file_path: String,
51 edit_index: usize,
52 },
53
54 #[error("invalid match selection for edit #{edit_index} in file {file_path}: {message}")]
55 InvalidMatchSelection {
56 file_path: String,
57 edit_index: usize,
58 message: String,
59 },
60
61 #[error(
62 "found {occurrences} matches for edit #{edit_index} in file {file_path}; old_string must match exactly once{preview_suffix}",
63 preview_suffix = non_unique_match_preview_suffix(match_previews, *omitted_matches)
64 )]
65 NonUniqueMatch {
66 file_path: String,
67 edit_index: usize,
68 occurrences: usize,
69 #[serde(default)]
70 match_previews: Vec<EditMatchPreview>,
71 #[serde(default)]
72 omitted_matches: usize,
73 },
74}
75
76#[derive(Deserialize, Serialize, Debug, JsonSchema, Clone, Error)]
77#[serde(tag = "code", content = "details", rename_all = "snake_case")]
78pub enum EditError {
79 #[error("{0}")]
80 Workspace(WorkspaceOpError),
81
82 #[error("{0}")]
83 EditFailure(EditFailure),
84}
85
86#[derive(Deserialize, Serialize, Debug, JsonSchema, Clone)]
87pub struct SingleEditOperation {
88 pub old_string: String,
90 pub new_string: String,
92 pub match_mode: Option<EditMatchMode>,
94 pub match_index: Option<u64>,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
99pub struct EditParams {
100 pub file_path: String,
102 pub old_string: String,
104 pub new_string: String,
106 pub match_mode: Option<EditMatchMode>,
108 pub match_index: Option<u64>,
110}
111
112pub mod multi_edit {
113 use super::{
114 Deserialize, EditFailure, Error, JsonSchema, MultiEditResult, Serialize,
115 SingleEditOperation, ToolExecutionError, ToolSpec, WorkspaceOpError,
116 };
117
118 pub const MULTI_EDIT_TOOL_NAME: &str = "multi_edit";
119
120 pub struct MultiEditToolSpec;
121
122 impl ToolSpec for MultiEditToolSpec {
123 type Params = MultiEditParams;
124 type Result = MultiEditResult;
125 type Error = MultiEditError;
126
127 const NAME: &'static str = MULTI_EDIT_TOOL_NAME;
128 const DISPLAY_NAME: &'static str = "Multi Edit";
129
130 fn execution_error(error: Self::Error) -> ToolExecutionError {
131 ToolExecutionError::MultiEdit(error)
132 }
133 }
134
135 #[derive(Deserialize, Serialize, Debug, JsonSchema, Clone, Error)]
136 #[serde(tag = "code", content = "details", rename_all = "snake_case")]
137 pub enum MultiEditError {
138 #[error("{0}")]
139 Workspace(WorkspaceOpError),
140
141 #[error("{0}")]
142 EditFailure(EditFailure),
143 }
144
145 #[derive(Deserialize, Serialize, Debug, JsonSchema)]
146 pub struct MultiEditParams {
147 pub file_path: String,
149 pub edits: Vec<SingleEditOperation>,
151 }
152}