2432 lines · 87.0 KB · Rust
RawDownload
use super::restore_file_from_disk_tool::RestoreFileFromDiskTool;use super::save_file_tool::SaveFileTool;use crate::{    AgentTool, Templates, Thread, ToolCallEventStream, ToolPermissionDecision,    decide_permission_for_path,    edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat},};use acp_thread::Diff;use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields};use anyhow::{Context as _, Result, anyhow};use cloud_llm_client::CompletionIntent;use collections::HashSet;use futures::{FutureExt as _, StreamExt as _};use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};use indoc::formatdoc;use language::language_settings::{self, FormatOnSave};use language::{LanguageRegistry, ToPoint};use language_model::LanguageModelToolResultContent;use paths;use project::lsp_store::{FormatTrigger, LspFormatTarget};use project::{Project, ProjectPath};use schemars::JsonSchema;use serde::{Deserialize, Serialize};use settings::Settings;use std::ffi::OsStr;use std::path::{Path, PathBuf};use std::sync::Arc;use ui::SharedString;use util::ResultExt;use util::rel_path::RelPath; 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 `terminal` tool with the 'mv' command 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 EditFileToolInput {    /// A one-line, user-friendly markdown description of the edit. This will be shown in the UI and also passed to another model to perform the edit.    ///    /// 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:    /// - 'edit': Make granular edits to an existing file.    /// - 'create': Create a new file if it doesn't exist.    /// - 'overwrite': Replace the entire contents of an existing file.    ///    /// When a file already exists or you just created it, prefer editing it as opposed to recreating it from scratch.    pub mode: EditFileMode,} #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]struct EditFileToolPartialInput {    #[serde(default)]    path: String,    #[serde(default)]    display_description: String,} #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]#[serde(rename_all = "lowercase")]#[schemars(inline)]pub enum EditFileMode {    Edit,    Create,    Overwrite,} #[derive(Debug, Serialize, Deserialize)]pub struct EditFileToolOutput {    #[serde(alias = "original_path")]    input_path: PathBuf,    new_text: String,    old_text: Arc<String>,    #[serde(default)]    diff: String,    #[serde(alias = "raw_output")]    edit_agent_output: EditAgentOutput,} impl From<EditFileToolOutput> for LanguageModelToolResultContent {    fn from(output: EditFileToolOutput) -> Self {        if output.diff.is_empty() {            "No edits were made.".into()        } else {            format!(                "Edited {}:\n\n```diff\n{}\n```",                output.input_path.display(),                output.diff            )            .into()        }    }} pub struct EditFileTool {    thread: WeakEntity<Thread>,    language_registry: Arc<LanguageRegistry>,    project: Entity<Project>,    templates: Arc<Templates>,} impl EditFileTool {    pub fn new(        project: Entity<Project>,        thread: WeakEntity<Thread>,        language_registry: Arc<LanguageRegistry>,        templates: Arc<Templates>,    ) -> Self {        Self {            project,            thread,            language_registry,            templates,        }    }     fn authorize(        &self,        input: &EditFileToolInput,        event_stream: &ToolCallEventStream,        cx: &mut App,    ) -> Task<Result<()>> {        authorize_file_edit(            Self::NAME,            &input.path,            &input.display_description,            &self.thread,            event_stream,            cx,        )    }} pub enum SensitiveSettingsKind {    Local,    Global,} /// Canonicalize a path, stripping the Windows extended-length path prefix (`\\?\`)/// that `std::fs::canonicalize` adds on Windows. This ensures that canonicalized/// paths can be compared with non-canonicalized paths via `starts_with`.fn safe_canonicalize(path: &Path) -> std::io::Result<PathBuf> {    let canonical = std::fs::canonicalize(path)?;    #[cfg(target_os = "windows")]    {        let s = canonical.to_string_lossy();        if let Some(stripped) = s.strip_prefix("\\\\?\\") {            return Ok(PathBuf::from(stripped));        }    }    Ok(canonical)} /// Returns the kind of sensitive settings location this path targets, if any:/// either inside a `.zed/` local-settings directory or inside the global config dir.pub fn sensitive_settings_kind(path: &Path) -> Option<SensitiveSettingsKind> {    let local_settings_folder = paths::local_settings_folder_name();    if path.components().any(|component| {        component.as_os_str() == <_ as AsRef<OsStr>>::as_ref(&local_settings_folder)    }) {        return Some(SensitiveSettingsKind::Local);    }     // Walk up the path hierarchy until we find an ancestor that exists and can    // be canonicalized, then reconstruct the path from there. This handles    // cases where multiple levels of subdirectories don't exist yet (e.g.    // ~/.config/zed/new_subdir/evil.json).    let canonical_path = {        let mut current: Option<&Path> = Some(path);        let mut suffix_components = Vec::new();        loop {            match current {                Some(ancestor) => match safe_canonicalize(ancestor) {                    Ok(canonical) => {                        let mut result = canonical;                        for component in suffix_components.into_iter().rev() {                            result.push(component);                        }                        break Some(result);                    }                    Err(_) => {                        if let Some(file_name) = ancestor.file_name() {                            suffix_components.push(file_name.to_os_string());                        }                        current = ancestor.parent();                    }                },                None => break None,            }        }    };    if let Some(canonical_path) = canonical_path {        let config_dir = safe_canonicalize(paths::config_dir())            .unwrap_or_else(|_| paths::config_dir().to_path_buf());        if canonical_path.starts_with(&config_dir) {            return Some(SensitiveSettingsKind::Global);        }    }     None} pub fn is_sensitive_settings_path(path: &Path) -> bool {    sensitive_settings_kind(path).is_some()} pub fn authorize_file_edit(    tool_name: &str,    path: &Path,    display_description: &str,    thread: &WeakEntity<Thread>,    event_stream: &ToolCallEventStream,    cx: &mut App,) -> Task<Result<()>> {    let path_str = path.to_string_lossy();    let settings = agent_settings::AgentSettings::get_global(cx);    let decision = decide_permission_for_path(tool_name, &path_str, settings);     if let ToolPermissionDecision::Deny(reason) = decision {        return Task::ready(Err(anyhow!("{}", reason)));    }     let explicitly_allowed = matches!(decision, ToolPermissionDecision::Allow);     if explicitly_allowed && !is_sensitive_settings_path(path) {        return Task::ready(Ok(()));    }     match sensitive_settings_kind(path) {        Some(SensitiveSettingsKind::Local) => {            let context = crate::ToolPermissionContext {                tool_name: tool_name.to_string(),                input_values: vec![path_str.to_string()],            };            return event_stream.authorize(                format!("{} (local settings)", display_description),                context,                cx,            );        }        Some(SensitiveSettingsKind::Global) => {            let context = crate::ToolPermissionContext {                tool_name: tool_name.to_string(),                input_values: vec![path_str.to_string()],            };            return event_stream.authorize(                format!("{} (settings)", display_description),                context,                cx,            );        }        None => {}    }     let Ok(project_path) = thread.read_with(cx, |thread, cx| {        thread.project().read(cx).find_project_path(path, cx)    }) else {        return Task::ready(Err(anyhow!("thread was dropped")));    };     if project_path.is_some() {        Task::ready(Ok(()))    } else {        let context = crate::ToolPermissionContext {            tool_name: tool_name.to_string(),            input_values: vec![path_str.to_string()],        };        event_stream.authorize(display_description, context, cx)    }} impl AgentTool for EditFileTool {    type Input = EditFileToolInput;    type Output = EditFileToolOutput;     const NAME: &'static str = "edit_file";     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 Some(input) =                    serde_json::from_value::<EditFileToolPartialInput>(raw_input).ok()                {                    let path = input.path.trim();                    if !path.is_empty() {                        return 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)                            .into();                    }                     let description = input.display_description.trim();                    if !description.is_empty() {                        return description.to_string().into();                    }                }                 DEFAULT_UI_TEXT.into()            }        }    }     fn run(        self: Arc<Self>,        input: Self::Input,        event_stream: ToolCallEventStream,        cx: &mut App,    ) -> Task<Result<Self::Output>> {        let Ok(project) = self            .thread            .read_with(cx, |thread, _cx| thread.project().clone())        else {            return Task::ready(Err(anyhow!("thread was dropped")));        };        let project_path = match resolve_path(&input, project.clone(), cx) {            Ok(path) => path,            Err(err) => return Task::ready(Err(anyhow!(err))),        };        let abs_path = project.read(cx).absolute_path(&project_path, cx);        if let Some(abs_path) = abs_path.clone() {            event_stream.update_fields(                ToolCallUpdateFields::new().locations(vec![acp::ToolCallLocation::new(abs_path)]),            );        }         let authorize = self.authorize(&input, &event_stream, cx);        cx.spawn(async move |cx: &mut AsyncApp| {            authorize.await?;             let (request, model, action_log) = self.thread.update(cx, |thread, cx| {                let request = thread.build_completion_request(CompletionIntent::ToolResults, cx);                (request, thread.model().cloned(), thread.action_log().clone())            })?;            let request = request?;            let model = model.context("No language model configured")?;             let edit_format = EditFormat::from_model(model.clone())?;            let edit_agent = EditAgent::new(                model,                project.clone(),                action_log.clone(),                self.templates.clone(),                edit_format,            );             let buffer = project                .update(cx, |project, cx| {                    project.open_buffer(project_path.clone(), cx)                })                .await?;             // Check if the file has been modified since the agent last read it            if let Some(abs_path) = abs_path.as_ref() {                let (last_read_mtime, current_mtime, is_dirty, has_save_tool, has_restore_tool) = self.thread.update(cx, |thread, cx| {                    let last_read = thread.file_read_times.get(abs_path).copied();                    let current = buffer.read(cx).file().and_then(|file| file.disk_state().mtime());                    let dirty = buffer.read(cx).is_dirty();                    let has_save = thread.has_tool(SaveFileTool::NAME);                    let has_restore = thread.has_tool(RestoreFileFromDiskTool::NAME);                    (last_read, current, dirty, has_save, has_restore)                })?;                 // Check for unsaved changes first - these indicate modifications we don't know about                if is_dirty {                    let message = match (has_save_tool, has_restore_tool) {                        (true, true) => {                            "This file has unsaved changes. Ask the user whether they want to keep or discard those changes. \                             If they want to keep them, ask for confirmation then use the save_file tool to save the file, then retry this edit. \                             If they want to discard them, ask for confirmation then use the restore_file_from_disk tool to restore the on-disk contents, then retry this edit."                        }                        (true, false) => {                            "This file has unsaved changes. Ask the user whether they want to keep or discard those changes. \                             If they want to keep them, ask for confirmation then use the save_file tool to save the file, then retry this edit. \                             If they want to discard them, ask the user to manually revert the file, then inform you when it's ok to proceed."                        }                        (false, true) => {                            "This file has unsaved changes. Ask the user whether they want to keep or discard those changes. \                             If they want to keep them, ask the user to manually save the file, then inform you when it's ok to proceed. \                             If they want to discard them, ask for confirmation then use the restore_file_from_disk tool to restore the on-disk contents, then retry this edit."                        }                        (false, false) => {                            "This file has unsaved changes. Ask the user whether they want to keep or discard those changes, \                             then ask them to save or revert the file manually and inform you when it's ok to proceed."                        }                    };                    anyhow::bail!("{}", message);                }                 // Check if the file was modified on disk since we last read it                if let (Some(last_read), Some(current)) = (last_read_mtime, current_mtime) {                    // MTime can be unreliable for comparisons, so our newtype intentionally                    // doesn't support comparing them. If the mtime at all different                    // (which could be because of a modification or because e.g. system clock changed),                    // we pessimistically assume it was modified.                    if current != last_read {                        anyhow::bail!(                            "The file {} has been modified since you last read it. \                             Please read the file again to get the current state before editing it.",                            input.path.display()                        );                    }                }            }             let diff = cx.new(|cx| Diff::new(buffer.clone(), cx));            event_stream.update_diff(diff.clone());            let _finalize_diff = util::defer({               let diff = diff.downgrade();               let mut cx = cx.clone();               move || {                   diff.update(&mut cx, |diff, cx| diff.finalize(cx)).ok();               }            });             let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());            let old_text = cx                .background_spawn({                    let old_snapshot = old_snapshot.clone();                    async move { Arc::new(old_snapshot.text()) }                })                .await;             let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) {                edit_agent.edit(                    buffer.clone(),                    input.display_description.clone(),                    &request,                    cx,                )            } else {                edit_agent.overwrite(                    buffer.clone(),                    input.display_description.clone(),                    &request,                    cx,                )            };             let mut hallucinated_old_text = false;            let mut ambiguous_ranges = Vec::new();            let mut emitted_location = false;            loop {

Lines 1–500 of 2432.

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