3926 lines · 141.9 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::{    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::{Context as _, 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, Serialize};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")]    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#[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.    ///    /// Always include complete lines. Do not start or end mid-line.    /// 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)]    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>,} #[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,    },} impl StreamingEditFileToolOutput {    pub fn error(error: impl Into<String>) -> Self {        Self::Error {            error: error.into(),        }    }} 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 } => write!(f, "{error}"),        }    }} 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>,} 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);            });        }    }} 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();                    let path = path.trim();                    if !path.is_empty() {                        return self                            .project                            .read(cx)                            .find_project_path(&path, cx)                            .and_then(|project_path| {                                self.project                                    .read(cx)                                    .short_full_path_for_project_path(&project_path, cx)                            })                            .unwrap_or_else(|| path.to_string())                            .into();                    }                     let description = input.display_description.unwrap_or_default();                    let description = description.trim();                    if !description.is_empty() {                        return description.to_string().into();                    }                }                 DEFAULT_UI_TEXT.into()            }        }    }     fn run(        self: Arc<Self>,        mut input: ToolInput<Self::Input>,        event_stream: ToolCallEventStream,        cx: &mut App,    ) -> Task<Result<Self::Output, Self::Output>> {        cx.spawn(async move |cx: &mut AsyncApp| {            let mut state: Option<EditSession> = None;            let mut last_partial: Option<StreamingEditFileToolPartialInput> = None;            loop {                futures::select! {                    partial = input.recv_partial().fuse() => {                        let Some(partial_value) = partial else { break };                        if let Ok(parsed) = serde_json::from_value::<StreamingEditFileToolPartialInput>(partial_value) {                            let path_complete = parsed.path.is_some()                                && parsed.path.as_ref() == last_partial.as_ref().and_then(|p| p.path.as_ref());                             last_partial = Some(parsed.clone());                             if state.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(session) => state = Some(session),                                    Err(e) => {                                        log::error!("Failed to create edit session: {}", e);                                        return Err(e);                                    }                                }                            }                             if let Some(state) = &mut state {                                if let Err(e) = state.process(parsed, &self, &event_stream, cx) {                                    log::error!("Failed to process edit: {}", e);                                    return Err(e);                                }                            }                        }                    }                    _ = event_stream.cancelled_by_user().fuse() => {                        return Err(StreamingEditFileToolOutput::error("Edit cancelled by user"));                    }                }            }            let full_input =                input                    .recv()                    .await                    .map_err(|e| {                        let err = StreamingEditFileToolOutput::error(format!("Failed to receive tool input: {e}"));                        log::error!("Failed to receive tool input: {e}");                        err                    })?;             let mut state = if let Some(state) = state {                state            } else {                match EditSession::new(                    &full_input.path,                    &full_input.display_description,                    full_input.mode,                    &self,                    &event_stream,                    cx,                )                .await                {                    Ok(session) => session,                    Err(e) => {                        log::error!("Failed to create edit session: {}", e);                        return Err(e);                    }                }            };            match state.finalize(full_input, &self, &event_stream, cx).await {                Ok(output) => Ok(output),                Err(e) => {                    log::error!("Failed to finalize edit: {}", e);                    Err(e)                }            }        })    }     fn replay(        &self,        _input: Self::Input,        output: Self::Output,        event_stream: ToolCallEventStream,        cx: &mut App,    ) -> Result<()> {        match output {            StreamingEditFileToolOutput::Success {                input_path,                old_text,                new_text,                ..            } => {                event_stream.update_diff(cx.new(|cx| {                    Diff::finalized(                        input_path.to_string_lossy().into_owned(),                        Some(old_text.to_string()),                        new_text,                        self.language_registry.clone(),                        cx,                    )                }));                Ok(())            }            StreamingEditFileToolOutput::Error { .. } => Ok(()),        }    }} pub struct EditSession {    abs_path: PathBuf,    buffer: Entity<Buffer>,    old_text: Arc<String>,    diff: Entity<Diff>,    mode: StreamingEditFileMode,    parser: ToolEditParser,    pipeline: EditPipeline,    _finalize_diff_guard: Deferred<Box<dyn FnOnce()>>,} struct EditPipeline {    current_edit: Option<EditPipelineEntry>,    content_written: bool,} enum EditPipelineEntry {    ResolvingOldText {        matcher: StreamingFuzzyMatcher,    },    StreamingNewText {        streaming_diff: StreamingDiff,        edit_cursor: usize,        reindenter: Reindenter,        original_snapshot: text::BufferSnapshot,    },} impl EditPipeline {    fn new() -> Self {        Self {            current_edit: None,            content_written: false,        }    }     fn ensure_resolving_old_text(&mut self, buffer: &Entity<Buffer>, cx: &mut AsyncApp) {        if self.current_edit.is_none() {            let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.text_snapshot());            self.current_edit = Some(EditPipelineEntry::ResolvingOldText {                matcher: StreamingFuzzyMatcher::new(snapshot),            });        }    }} impl EditSession {    async fn new(        path: &PathBuf,        display_description: &str,        mode: StreamingEditFileMode,        tool: &StreamingEditFileTool,        event_stream: &ToolCallEventStream,        cx: &mut AsyncApp,    ) -> Result<Self, StreamingEditFileToolOutput> {        let project_path = cx            .update(|cx| resolve_path(mode, &path, &tool.project, cx))            .map_err(|e| StreamingEditFileToolOutput::error(e.to_string()))?;         let Some(abs_path) = cx.update(|cx| tool.project.read(cx).absolute_path(&project_path, cx))        else {            return Err(StreamingEditFileToolOutput::error(format!(                "Worktree at '{}' does not exist",

Lines 1–500 of 3926.