Skip to main content

steer_tools/tools/
edit.rs

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    /// The exact string to find and replace. Must be non-empty and match according to `match_mode`.
89    pub old_string: String,
90    /// The string to replace `old_string` with.
91    pub new_string: String,
92    /// Optional match mode for this edit. Defaults to `exactly_one` when omitted.
93    pub match_mode: Option<EditMatchMode>,
94    /// Optional 1-based match index used when `match_mode` is `nth`.
95    pub match_index: Option<u64>,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
99pub struct EditParams {
100    /// The absolute path to the file to edit
101    pub file_path: String,
102    /// The exact string to find and replace. Must be non-empty.
103    pub old_string: String,
104    /// The string to replace `old_string` with.
105    pub new_string: String,
106    /// Optional match mode for this edit. Defaults to `exactly_one` when omitted.
107    pub match_mode: Option<EditMatchMode>,
108    /// Optional 1-based match index used when `match_mode` is `nth`.
109    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        /// The absolute path to the file to edit.
148        pub file_path: String,
149        /// A list of edit operations to apply sequentially.
150        pub edits: Vec<SingleEditOperation>,
151    }
152}