1701 lines · 60.5 KB · Rust
RawDownload
use crate::{    Templates,    edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat},    schema::json_schema_for,    ui::{COLLAPSED_LINES, ToolOutputPreview},};use anyhow::{Context as _, Result, anyhow};use assistant_tool::{    ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput,    ToolUseStatus,};use buffer_diff::{BufferDiff, BufferDiffSnapshot};use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, scroll::Autoscroll};use futures::StreamExt;use gpui::{    Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,    TextStyleRefinement, WeakEntity, pulsating_between, px,};use indoc::formatdoc;use language::{    Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Point, Rope,    TextBuffer,    language_settings::{self, FormatOnSave, SoftWrap},};use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};use markdown::{Markdown, MarkdownElement, MarkdownStyle};use project::{    Project, ProjectPath,    lsp_store::{FormatTrigger, LspFormatTarget},};use schemars::JsonSchema;use serde::{Deserialize, Serialize};use settings::Settings;use std::{    cmp::Reverse,    collections::HashSet,    ops::Range,    path::{Path, PathBuf},    sync::Arc,    time::Duration,};use theme::ThemeSettings;use ui::{Disclosure, Tooltip, prelude::*};use util::ResultExt;use workspace::Workspace; pub struct EditFileTool; #[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)]#[serde(rename_all = "lowercase")]pub enum EditFileMode {    Edit,    Create,    Overwrite,} #[derive(Debug, Serialize, Deserialize, JsonSchema)]pub struct EditFileToolOutput {    pub original_path: PathBuf,    pub new_text: String,    pub old_text: Arc<String>,    pub raw_output: Option<EditAgentOutput>,} #[derive(Debug, Serialize, Deserialize, JsonSchema)]struct PartialInput {    #[serde(default)]    path: String,    #[serde(default)]    display_description: String,} const DEFAULT_UI_TEXT: &str = "Editing file"; impl Tool for EditFileTool {    fn name(&self) -> String {        "edit_file".into()    }     fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {        false    }     fn may_perform_edits(&self) -> bool {        true    }     fn description(&self) -> String {        include_str!("edit_file_tool/description.md").to_string()    }     fn icon(&self) -> IconName {        IconName::Pencil    }     fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {        json_schema_for::<EditFileToolInput>(format)    }     fn ui_text(&self, input: &serde_json::Value) -> String {        match serde_json::from_value::<EditFileToolInput>(input.clone()) {            Ok(input) => input.display_description,            Err(_) => "Editing file".to_string(),        }    }     fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String {        if let Some(input) = serde_json::from_value::<PartialInput>(input.clone()).ok() {            let description = input.display_description.trim();            if !description.is_empty() {                return description.to_string();            }             let path = input.path.trim();            if !path.is_empty() {                return path.to_string();            }        }         DEFAULT_UI_TEXT.to_string()    }     fn run(        self: Arc<Self>,        input: serde_json::Value,        request: Arc<LanguageModelRequest>,        project: Entity<Project>,        action_log: Entity<ActionLog>,        model: Arc<dyn LanguageModel>,        window: Option<AnyWindowHandle>,        cx: &mut App,    ) -> ToolResult {        let input = match serde_json::from_value::<EditFileToolInput>(input) {            Ok(input) => input,            Err(err) => return Task::ready(Err(anyhow!(err))).into(),        };         let project_path = match resolve_path(&input, project.clone(), cx) {            Ok(path) => path,            Err(err) => return Task::ready(Err(anyhow!(err))).into(),        };         let card = window.and_then(|window| {            window                .update(cx, |_, window, cx| {                    cx.new(|cx| {                        EditFileToolCard::new(input.path.clone(), project.clone(), window, cx)                    })                })                .ok()        });         let card_clone = card.clone();        let action_log_clone = action_log.clone();        let task = cx.spawn(async move |cx: &mut AsyncApp| {            let edit_format = EditFormat::from_model(model.clone())?;            let edit_agent = EditAgent::new(                model,                project.clone(),                action_log_clone,                Templates::new(),                edit_format,            );             let buffer = project                .update(cx, |project, cx| {                    project.open_buffer(project_path.clone(), cx)                })?                .await?;             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;             if let Some(card) = card_clone.as_ref() {                card.update(cx, |card, cx| card.initialize(buffer.clone(), cx))?;            }             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();            while let Some(event) = events.next().await {                match event {                    EditAgentOutputEvent::Edited => {                        if let Some(card) = card_clone.as_ref() {                            card.update(cx, |card, cx| card.update_diff(cx))?;                        }                    }                    EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true,                    EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges,                    EditAgentOutputEvent::ResolvingEditRange(range) => {                        if let Some(card) = card_clone.as_ref() {                            card.update(cx, |card, cx| card.reveal_range(range, cx))?;                        }                    }                }            }            let agent_output = output.await?;             // If format_on_save is enabled, format the buffer            let format_on_save_enabled = buffer                .read_with(cx, |buffer, cx| {                    let settings = language_settings::language_settings(                        buffer.language().map(|l| l.name()),                        buffer.file(),                        cx,                    );                    !matches!(settings.format_on_save, FormatOnSave::Off)                })                .unwrap_or(false);             if format_on_save_enabled {                let format_task = project.update(cx, |project, cx| {                    project.format(                        HashSet::from_iter([buffer.clone()]),                        LspFormatTarget::Buffers,                        false, // Don't push to history since the tool did it.                        FormatTrigger::Save,                        cx,                    )                })?;                format_task.await.log_err();            }             project                .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?                .await?;             // Notify the action log that we've edited the buffer (*after* formatting has completed).            action_log.update(cx, |log, cx| {                log.buffer_edited(buffer.clone(), cx);            })?;             let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;            let (new_text, diff) = cx                .background_spawn({                    let new_snapshot = new_snapshot.clone();                    let old_text = old_text.clone();                    async move {                        let new_text = new_snapshot.text();                        let diff = language::unified_diff(&old_text, &new_text);                         (new_text, diff)                    }                })                .await;             let output = EditFileToolOutput {                original_path: project_path.path.to_path_buf(),                new_text: new_text.clone(),                old_text,                raw_output: Some(agent_output),            };             if let Some(card) = card_clone {                card.update(cx, |card, cx| {                    card.update_diff(cx);                    card.finalize(cx)                })                .log_err();            }             let input_path = input.path.display();            if diff.is_empty() {                anyhow::ensure!(                    !hallucinated_old_text,                    formatdoc! {"                        Some edits were produced but none of them could be applied.                        Read the relevant sections of {input_path} again so that                        I can perform the requested edits.                    "}                );                anyhow::ensure!(                    ambiguous_ranges.is_empty(),                    {                        let line_numbers = ambiguous_ranges                            .iter()                            .map(|range| range.start.to_string())                            .collect::<Vec<_>>()                            .join(", ");                        formatdoc! {"                            <old_text> matches more than one position in the file (lines: {line_numbers}). Read the                            relevant sections of {input_path} again and extend <old_text> so                            that I can perform the requested edits.                        "}                    }                );                Ok(ToolResultOutput {                    content: ToolResultContent::Text("No edits were made.".into()),                    output: serde_json::to_value(output).ok(),                })            } else {                Ok(ToolResultOutput {                    content: ToolResultContent::Text(format!(                        "Edited {}:\n\n```diff\n{}\n```",                        input_path, diff                    )),                    output: serde_json::to_value(output).ok(),                })            }        });         ToolResult {            output: task,            card: card.map(AnyToolCard::from),        }    }     fn deserialize_card(        self: Arc<Self>,        output: serde_json::Value,        project: Entity<Project>,        window: &mut Window,        cx: &mut App,    ) -> Option<AnyToolCard> {        let output = match serde_json::from_value::<EditFileToolOutput>(output) {            Ok(output) => output,            Err(_) => return None,        };         let card = cx.new(|cx| {            EditFileToolCard::new(output.original_path.clone(), project.clone(), window, cx)        });         cx.spawn({            let path: Arc<Path> = output.original_path.into();            let language_registry = project.read(cx).languages().clone();            let card = card.clone();            async move |cx| {                let buffer =                    build_buffer(output.new_text, path.clone(), &language_registry, cx).await?;                let buffer_diff =                    build_buffer_diff(output.old_text.clone(), &buffer, &language_registry, cx)                        .await?;                card.update(cx, |card, cx| {                    card.multibuffer.update(cx, |multibuffer, cx| {                        let snapshot = buffer.read(cx).snapshot();                        let diff = buffer_diff.read(cx);                        let diff_hunk_ranges = diff                            .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)                            .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))                            .collect::<Vec<_>>();                         multibuffer.set_excerpts_for_path(                            PathKey::for_buffer(&buffer, cx),                            buffer,                            diff_hunk_ranges,                            editor::DEFAULT_MULTIBUFFER_CONTEXT,                            cx,                        );                        multibuffer.add_diff(buffer_diff, cx);                        let end = multibuffer.len(cx);                        card.total_lines =                            Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1);                    });                     cx.notify();                })?;                anyhow::Ok(())            }        })        .detach_and_log_err(cx);         Some(card.into())    }} /// Validate that the file path is valid, meaning:////// - For `edit` and `overwrite`, the path must point to an existing file./// - For `create`, the file must not already exist, but it's parent dir must exist.fn resolve_path(    input: &EditFileToolInput,    project: Entity<Project>,    cx: &mut App,) -> Result<ProjectPath> {    let project = project.read(cx);     match input.mode {        EditFileMode::Edit | EditFileMode::Overwrite => {            let path = project                .find_project_path(&input.path, cx)                .context("Can't edit file: path not found")?;             let entry = project                .entry_for_path(&path, cx)                .context("Can't edit file: path not found")?;             anyhow::ensure!(entry.is_file(), "Can't edit file: path is a directory");            Ok(path)        }         EditFileMode::Create => {            if let Some(path) = project.find_project_path(&input.path, cx) {                anyhow::ensure!(                    project.entry_for_path(&path, cx).is_none(),                    "Can't create file: file already exists"                );            }             let parent_path = input                .path                .parent()                .context("Can't create file: incorrect path")?;             let parent_project_path = project.find_project_path(&parent_path, cx);             let parent_entry = parent_project_path                .as_ref()                .and_then(|path| project.entry_for_path(&path, cx))                .context("Can't create file: parent directory doesn't exist")?;             anyhow::ensure!(                parent_entry.is_dir(),                "Can't create file: parent is not a directory"            );             let file_name = input                .path                .file_name()                .context("Can't create file: invalid filename")?;             let new_file_path = parent_project_path.map(|parent| ProjectPath {                path: Arc::from(parent.path.join(file_name)),                ..parent            });             new_file_path.context("Can't create file")        }    }} pub struct EditFileToolCard {

Lines 1–500 of 1701.