4250 lines · 153.7 KB · Rust
RawDownload
use super::edit_file_tool::EditFileTool;use super::restore_file_from_disk_tool::RestoreFileFromDiskTool;use super::save_file_tool::SaveFileTool;use super::tool_edit_parser::{ToolEditEvent, ToolEditParser};use crate::ToolInputPayload;use crate::{    AgentTool, Thread, ToolCallEventStream, ToolInput,    edit_agent::{        reindent::{Reindenter, compute_indent_delta},        streaming_fuzzy_matcher::StreamingFuzzyMatcher,    },};use acp_thread::Diff;use action_log::ActionLog;use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields};use anyhow::Result;use collections::HashSet;use futures::FutureExt as _;use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};use language::language_settings::{self, FormatOnSave};use language::{Buffer, LanguageRegistry};use language_model::LanguageModelToolResultContent;use project::lsp_store::{FormatTrigger, LspFormatTarget};use project::{AgentLocation, Project, ProjectPath};use schemars::JsonSchema;use serde::{    Deserialize, Deserializer, Serialize,    de::{DeserializeOwned, Error as _},};use std::ops::Range;use std::path::PathBuf;use std::sync::Arc;use streaming_diff::{CharOperation, StreamingDiff};use text::ToOffset;use ui::SharedString;use util::rel_path::RelPath;use util::{Deferred, ResultExt}; const DEFAULT_UI_TEXT: &str = "Editing file"; /// This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `move_path` tool instead.////// Before using this tool:////// 1. Use the `read_file` tool to understand the file's contents and context////// 2. Verify the directory path is correct (only applicable when creating new files):///    - Use the `list_directory` tool to verify the parent directory exists and is the correct location#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]pub struct StreamingEditFileToolInput {    /// A one-line, user-friendly markdown description of the edit. This will be shown in the UI.    ///    /// Be terse, but also descriptive in what you want to achieve with this edit. Avoid generic instructions.    ///    /// NEVER mention the file path in this description.    ///    /// <example>Fix API endpoint URLs</example>    /// <example>Update copyright year in `page_footer`</example>    ///    /// Make sure to include this field before all the others in the input object so that we can display it immediately.    pub display_description: String,     /// The full path of the file to create or modify in the project.    ///    /// WARNING: When specifying which file path need changing, you MUST start each path with one of the project's root directories.    ///    /// The following examples assume we have two root directories in the project:    /// - /a/b/backend    /// - /c/d/frontend    ///    /// <example>    /// `backend/src/main.rs`    ///    /// Notice how the file path starts with `backend`. Without that, the path would be ambiguous and the call would fail!    /// </example>    ///    /// <example>    /// `frontend/db.js`    /// </example>    pub path: PathBuf,     /// The mode of operation on the file. Possible values:    /// - 'write': Replace the entire contents of the file. If the file doesn't exist, it will be created. Requires 'content' field.    /// - 'edit': Make granular edits to an existing file. Requires 'edits' field.    ///    /// When a file already exists or you just created it, prefer editing it as opposed to recreating it from scratch.    pub mode: StreamingEditFileMode,     /// The complete content for the new file (required for 'write' mode).    /// This field should contain the entire file content.    #[serde(default, skip_serializing_if = "Option::is_none")]    pub content: Option<String>,     /// List of edit operations to apply sequentially (required for 'edit' mode).    /// Each edit finds `old_text` in the file and replaces it with `new_text`.    #[serde(        default,        skip_serializing_if = "Option::is_none",        deserialize_with = "deserialize_optional_vec_or_json_string"    )]    pub edits: Option<Vec<Edit>>,} #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]#[serde(rename_all = "snake_case")]pub enum StreamingEditFileMode {    /// Overwrite the file with new content (replacing any existing content).    /// If the file does not exist, it will be created.    Write,    /// Make granular edits to an existing file    Edit,} /// A single edit operation that replaces old text with new text/// Properly escape all text fields as valid JSON strings./// Remember to escape special characters like newlines (`\n`) and quotes (`"`) in JSON strings.#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]pub struct Edit {    /// The exact text to find in the file. This will be matched using fuzzy matching    /// to handle minor differences in whitespace or formatting.    ///    /// Be minimal with replacements:    /// - For unique lines, include only those lines    /// - For non-unique lines, include enough context to identify them    pub old_text: String,    /// The text to replace it with    pub new_text: String,} #[derive(Clone, Default, Debug, Deserialize)]struct StreamingEditFileToolPartialInput {    #[serde(default)]    display_description: Option<String>,    #[serde(default)]    path: Option<String>,    #[serde(default)]    mode: Option<StreamingEditFileMode>,    #[serde(default)]    content: Option<String>,    #[serde(default, deserialize_with = "deserialize_optional_vec_or_json_string")]    edits: Option<Vec<PartialEdit>>,} #[derive(Clone, Default, Debug, Deserialize)]pub struct PartialEdit {    #[serde(default)]    pub old_text: Option<String>,    #[serde(default)]    pub new_text: Option<String>,} /// Sometimes the model responds with a stringified JSON array of edits (`"[...]"`) instead of a regular array (`[...]`)fn deserialize_optional_vec_or_json_string<'de, T, D>(    deserializer: D,) -> Result<Option<Vec<T>>, D::Error>where    T: DeserializeOwned,    D: Deserializer<'de>,{    #[derive(Deserialize)]    #[serde(untagged)]    enum VecOrJsonString<T> {        Vec(Vec<T>),        String(String),    }     let value = Option::<VecOrJsonString<T>>::deserialize(deserializer)?;    match value {        None => Ok(None),        Some(VecOrJsonString::Vec(items)) => Ok(Some(items)),        Some(VecOrJsonString::String(string)) => serde_json::from_str::<Vec<T>>(&string)            .map(Some)            .map_err(|error| {                D::Error::custom(format!("failed to parse stringified edits array: {error}"))            }),    }} #[derive(Debug, Serialize, Deserialize)]#[serde(untagged)]pub enum StreamingEditFileToolOutput {    Success {        #[serde(alias = "original_path")]        input_path: PathBuf,        new_text: String,        old_text: Arc<String>,        #[serde(default)]        diff: String,    },    Error {        error: String,        #[serde(default)]        input_path: Option<PathBuf>,        #[serde(default)]        diff: String,    },} impl StreamingEditFileToolOutput {    pub fn error(error: impl Into<String>) -> Self {        Self::Error {            error: error.into(),            input_path: None,            diff: String::new(),        }    }} impl std::fmt::Display for StreamingEditFileToolOutput {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        match self {            StreamingEditFileToolOutput::Success {                diff, input_path, ..            } => {                if diff.is_empty() {                    write!(f, "No edits were made.")                } else {                    write!(                        f,                        "Edited {}:\n\n```diff\n{diff}\n```",                        input_path.display()                    )                }            }            StreamingEditFileToolOutput::Error {                error,                diff,                input_path,            } => {                write!(f, "{error}\n")?;                if let Some(input_path) = input_path                    && !diff.is_empty()                {                    write!(                        f,                        "Edited {}:\n\n```diff\n{diff}\n```",                        input_path.display()                    )                } else {                    write!(f, "No edits were made.")                }            }        }    }} impl From<StreamingEditFileToolOutput> for LanguageModelToolResultContent {    fn from(output: StreamingEditFileToolOutput) -> Self {        output.to_string().into()    }} pub struct StreamingEditFileTool {    project: Entity<Project>,    thread: WeakEntity<Thread>,    action_log: Entity<ActionLog>,    language_registry: Arc<LanguageRegistry>,} enum EditSessionResult {    Completed(EditSession),    Failed {        error: String,        session: Option<EditSession>,    },} impl StreamingEditFileTool {    pub fn new(        project: Entity<Project>,        thread: WeakEntity<Thread>,        action_log: Entity<ActionLog>,        language_registry: Arc<LanguageRegistry>,    ) -> Self {        Self {            project,            thread,            action_log,            language_registry,        }    }     fn authorize(        &self,        path: &PathBuf,        description: &str,        event_stream: &ToolCallEventStream,        cx: &mut App,    ) -> Task<Result<()>> {        super::tool_permissions::authorize_file_edit(            EditFileTool::NAME,            path,            description,            &self.thread,            event_stream,            cx,        )    }     fn set_agent_location(&self, buffer: WeakEntity<Buffer>, position: text::Anchor, cx: &mut App) {        let should_update_agent_location = self            .thread            .read_with(cx, |thread, _cx| !thread.is_subagent())            .unwrap_or_default();        if should_update_agent_location {            self.project.update(cx, |project, cx| {                project.set_agent_location(Some(AgentLocation { buffer, position }), cx);            });        }    }     async fn ensure_buffer_saved(&self, buffer: &Entity<Buffer>, cx: &mut AsyncApp) {        let format_on_save_enabled = buffer.read_with(cx, |buffer, cx| {            let settings = language_settings::LanguageSettings::for_buffer(buffer, cx);            settings.format_on_save != FormatOnSave::Off        });         if format_on_save_enabled {            self.project                .update(cx, |project, cx| {                    project.format(                        HashSet::from_iter([buffer.clone()]),                        LspFormatTarget::Buffers,                        false,                        FormatTrigger::Save,                        cx,                    )                })                .await                .log_err();        }         self.project            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))            .await            .log_err();         self.action_log.update(cx, |log, cx| {            log.buffer_edited(buffer.clone(), cx);        });    }     async fn process_streaming_edits(        &self,        input: &mut ToolInput<StreamingEditFileToolInput>,        event_stream: &ToolCallEventStream,        cx: &mut AsyncApp,    ) -> EditSessionResult {        let mut session: Option<EditSession> = None;        let mut last_partial: Option<StreamingEditFileToolPartialInput> = None;         loop {            futures::select! {                payload = input.next().fuse() => {                    match payload {                        Ok(payload) => match payload {                            ToolInputPayload::Partial(partial) => {                                if let Ok(parsed) = serde_json::from_value::<StreamingEditFileToolPartialInput>(partial) {                                    let path_complete = parsed.path.is_some()                                        && parsed.path.as_ref() == last_partial.as_ref().and_then(|partial| partial.path.as_ref());                                     last_partial = Some(parsed.clone());                                     if session.is_none()                                        && path_complete                                        && let StreamingEditFileToolPartialInput {                                            path: Some(path),                                            display_description: Some(display_description),                                            mode: Some(mode),                                            ..                                        } = &parsed                                    {                                        match EditSession::new(                                            PathBuf::from(path),                                            display_description,                                            *mode,                                            self,                                            event_stream,                                            cx,                                        )                                        .await                                        {                                            Ok(created_session) => session = Some(created_session),                                            Err(error) => {                                                log::error!("Failed to create edit session: {}", error);                                                return EditSessionResult::Failed {                                                    error,                                                    session: None,                                                };                                            }                                        }                                    }                                     if let Some(current_session) = &mut session                                        && let Err(error) = current_session.process(parsed, self, event_stream, cx)                                    {                                        log::error!("Failed to process edit: {}", error);                                        return EditSessionResult::Failed { error, session };                                    }                                }                            }                            ToolInputPayload::Full(full_input) => {                                let mut session = if let Some(session) = session {                                    session                                } else {                                    match EditSession::new(                                        full_input.path.clone(),                                        &full_input.display_description,                                        full_input.mode,                                        self,                                        event_stream,                                        cx,                                    )                                    .await                                    {                                        Ok(created_session) => created_session,                                        Err(error) => {                                            log::error!("Failed to create edit session: {}", error);                                            return EditSessionResult::Failed {                                                error,                                                session: None,                                            };                                        }                                    }                                };                                 return match session.finalize(full_input, self, event_stream, cx).await {                                    Ok(()) => EditSessionResult::Completed(session),                                    Err(error) => {                                        log::error!("Failed to finalize edit: {}", error);                                        EditSessionResult::Failed {                                            error,                                            session: Some(session),                                        }                                    }                                };                            }                            ToolInputPayload::InvalidJson { error_message } => {                                log::error!("Received invalid JSON: {error_message}");                                return EditSessionResult::Failed {                                    error: error_message,                                    session,                                };                            }                        },                        Err(error) => {                            return EditSessionResult::Failed {                                error: format!("Failed to receive tool input: {error}"),                                session,                            };                        }                    }                }                _ = event_stream.cancelled_by_user().fuse() => {                    return EditSessionResult::Failed {                        error: "Edit cancelled by user".to_string(),                        session,                    };                }            }        }    }} impl AgentTool for StreamingEditFileTool {    type Input = StreamingEditFileToolInput;    type Output = StreamingEditFileToolOutput;     const NAME: &'static str = "streaming_edit_file";     fn supports_input_streaming() -> bool {        true    }     fn kind() -> acp::ToolKind {        acp::ToolKind::Edit    }     fn initial_title(        &self,        input: Result<Self::Input, serde_json::Value>,        cx: &mut App,    ) -> SharedString {        match input {            Ok(input) => self                .project                .read(cx)                .find_project_path(&input.path, cx)                .and_then(|project_path| {                    self.project                        .read(cx)                        .short_full_path_for_project_path(&project_path, cx)                })                .unwrap_or(input.path.to_string_lossy().into_owned())                .into(),            Err(raw_input) => {                if let Ok(input) =                    serde_json::from_value::<StreamingEditFileToolPartialInput>(raw_input)                {                    let path = input.path.unwrap_or_default();

Lines 1–500 of 4250.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t