Richard Feldmanwipd97b1fc1 year agoHistory
581 lines · 20.8 KB · Rust
RawDownload
use crate::{replace::replace_with_flexible_indent, schema::json_schema_for};use anyhow::{Context as _, Result, anyhow};use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};use buffer_diff::{BufferDiff, BufferDiffSnapshot};use editor::{Editor, MultiBuffer, PathKey};use gpui::{    AnyWindowHandle, App, AppContext, AsyncApp, Context, Entity, IntoElement, Task, Window,};use language::{    self, Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt as _, Rope,    TextBuffer,};use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};use project::Project;use schemars::JsonSchema;use serde::{Deserialize, Serialize};use std::{    path::{Path, PathBuf},    sync::Arc,};use ui::{Tooltip, prelude::*};use util::ResultExt; use crate::replace::replace_exact; #[derive(Debug, Serialize, Deserialize, JsonSchema)]pub struct FindReplaceFileToolInput {    /// The path of the file to modify.    ///    /// 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:    /// - backend    /// - frontend    ///    /// <example>    /// `backend/src/main.rs`    ///    /// Notice how the file path starts with root-1. Without that, the path    /// would be ambiguous and the call would fail!    /// </example>    ///    /// <example>    /// `frontend/db.js`    /// </example>    pub path: PathBuf,     /// A user-friendly markdown description of what's being replaced. This will be shown in the UI.    ///    /// <example>Fix API endpoint URLs</example>    /// <example>Update copyright year in `page_footer`</example>    pub display_description: String,     /// The unique string to find in the file. This string cannot be empty;    /// if the string is empty, the tool call will fail. Remember, do not use this tool    /// to create new files from scratch, or to overwrite existing files! Use a different    /// approach if you want to do that.    ///    /// If this string appears more than once in the file, this tool call will fail,    /// so it is absolutely critical that you verify ahead of time that the string    /// is unique. You can search within the file to verify this.    ///    /// To make the string more likely to be unique, include a minimum of 3 lines of context    /// before the string you actually want to find, as well as a minimum of 3 lines of    /// context after the string you want to find. (These lines of context should appear    /// in the `replace` string as well.) If 3 lines of context is not enough to obtain    /// a string that appears only once in the file, then double the number of context lines    /// until the string becomes unique. (Start with 3 lines before and 3 lines after    /// though, because too much context is needlessly costly.)    ///    /// Do not alter the context lines of code in any way, and make sure to preserve all    /// whitespace and indentation for all lines of code. This string must be exactly as    /// it appears in the file, because this tool will do a literal find/replace, and if    /// even one character in this string is different in any way from how it appears    /// in the file, then the tool call will fail.    ///    /// If you get an error that the `find` string was not found, this means that either    /// you made a mistake, or that the file has changed since you last looked at it.    /// Either way, when this happens, you should retry doing this tool call until it    /// succeeds, up to 3 times. Each time you retry, you should take another look at    /// the exact text of the file in question, to make sure that you are searching for    /// exactly the right string. Regardless of whether it was because you made a mistake    /// or because the file changed since you last looked at it, you should be extra    /// careful when retrying in this way. It's a bad experience for the user if    /// this `find` string isn't found, so be super careful to get it exactly right!    ///    /// <example>    /// If a file contains this code:    ///    /// ```ignore    /// fn check_user_permissions(user_id: &str) -> Result<bool> {    ///     // Check if user exists first    ///     let user = database.find_user(user_id)?;    ///    ///     // This is the part we want to modify    ///     if user.role == "admin" {    ///         return Ok(true);    ///     }    ///    ///     // Check other permissions    ///     check_custom_permissions(user_id)    /// }    /// ```    ///    /// Your find string should include at least 3 lines of context before and after the part    /// you want to change:    ///    /// ```ignore    /// fn check_user_permissions(user_id: &str) -> Result<bool> {    ///     // Check if user exists first    ///     let user = database.find_user(user_id)?;    ///    ///     // This is the part we want to modify    ///     if user.role == "admin" {    ///         return Ok(true);    ///     }    ///    ///     // Check other permissions    ///     check_custom_permissions(user_id)    /// }    /// ```    ///    /// And your replace string might look like:    ///    /// ```ignore    /// fn check_user_permissions(user_id: &str) -> Result<bool> {    ///     // Check if user exists first    ///     let user = database.find_user(user_id)?;    ///    ///     // This is the part we want to modify    ///     if user.role == "admin" || user.role == "superuser" {    ///         return Ok(true);    ///     }    ///    ///     // Check other permissions    ///     check_custom_permissions(user_id)    /// }    /// ```    /// </example>    pub find: String,     /// The string to replace the one unique occurrence of the find string with.    pub replace: String,} pub struct FindReplaceFileToolCard {    path: PathBuf,    description: String,    editor: Entity<Editor>,    multibuffer: Entity<MultiBuffer>,    project: Entity<Project>,    diff_task: Option<Task<Result<()>>>,} impl FindReplaceFileToolCard {    fn new(        path: PathBuf,        description: String,        project: Entity<Project>,        window: &mut Window,        cx: &mut App,    ) -> Self {        let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadOnly));        let editor = cx.new(|cx| {            let mut editor =                Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);            editor.disable_inline_diagnostics();            editor.set_expand_all_diff_hunks(cx);            editor        });         Self {            path,            description,            project,            editor,            multibuffer,            diff_task: None,        }    }     fn set_diff(        &mut self,        path: Arc<Path>,        old_text: String,        new_text: String,        cx: &mut Context<Self>,    ) {        let language_registry = self.project.read(cx).languages().clone();        self.diff_task = Some(cx.spawn(async move |this, cx| {            let buffer = build_buffer(new_text, path.clone(), &language_registry, cx).await?;            let buffer_diff = build_buffer_diff(old_text, &buffer, &language_registry, cx).await?;             this.update(cx, |this, cx| {                this.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<_>>();                    let _is_newly_added = 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);                });                cx.notify();            })        }));    }} async fn build_buffer(    mut text: String,    path: Arc<Path>,    language_registry: &Arc<language::LanguageRegistry>,    cx: &mut AsyncApp,) -> Result<Entity<Buffer>> {    let line_ending = LineEnding::detect(&text);    LineEnding::normalize(&mut text);    let text = Rope::from(text);    let language = cx        .update(|_cx| language_registry.language_for_file_path(&path))?        .await        .ok();    let buffer = cx.new(|cx| {        let buffer = TextBuffer::new_normalized(            0,            cx.entity_id().as_non_zero_u64().into(),            line_ending,            text,        );        let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);        buffer.set_language(language, cx);        buffer    })?;    Ok(buffer)} async fn build_buffer_diff(    mut old_text: String,    buffer: &Entity<Buffer>,    language_registry: &Arc<LanguageRegistry>,    cx: &mut AsyncApp,) -> Result<Entity<BufferDiff>> {    LineEnding::normalize(&mut old_text);     let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;     let base_buffer = cx        .update(|cx| {            Buffer::build_snapshot(                old_text.clone().into(),                buffer.language().cloned(),                Some(language_registry.clone()),                cx,            )        })?        .await;     let diff_snapshot = cx        .update(|cx| {            BufferDiffSnapshot::new_with_base_buffer(                buffer.text.clone(),                Some(old_text.into()),                base_buffer,                cx,            )        })?        .await;     cx.new(|cx| {        let mut diff = BufferDiff::new(&buffer.text, cx);        diff.set_snapshot(diff_snapshot, &buffer.text, cx);        diff    })} impl ToolCard for FindReplaceFileToolCard {    fn render(        &mut self,        status: &ToolUseStatus,        _window: &mut Window,        cx: &mut Context<Self>,    ) -> impl IntoElement {        let header = h_flex()            .id("tool-label-container")            .gap_1p5()            .max_w_full()            .overflow_x_scroll()            .child(                Icon::new(IconName::Pencil)                    .size(IconSize::XSmall)                    .color(Color::Muted),            )            .child(Label::new("Edit ").size(LabelSize::Small))            .child(                div()                    .size(px(3.))                    .rounded_full()                    .bg(cx.theme().colors().text),            )            .child(Label::new(self.path.display().to_string()).size(LabelSize::Small))            .into_any_element();         let header2 = h_flex()            .id("code-block-header-label")            .w_full()            .max_w_full()            .px_1()            .gap_0p5()            .cursor_pointer()            .rounded_sm()            .hover(|item| item.bg(cx.theme().colors().element_hover.opacity(0.5)))            .tooltip(Tooltip::text("Jump to File"));        // todo!        // .child(        //     h_flex()        //         .gap_0p5()        //         .children(        //             file_icons::FileIcons::get_icon(&path_range.path, cx)        //                 .map(Icon::from_path)        //                 .map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),        //         )        //         .child(content)        //         .child(        //             Icon::new(IconName::ArrowUpRight)        //                 .size(IconSize::XSmall)        //                 .color(Color::Ignored),        //         ),        // )        // .on_click({        //     let path_range = path_range.clone();        //     move |_, window, cx| {        //         workspace        //             .update(cx, {        //                 |workspace, cx| {        //                     if let Some(project_path) = workspace        //                         .project()        //                         .read(cx)        //                         .find_project_path(&path_range.path, cx)        //                     {        //                         let target = path_range.range.as_ref().map(|range| {        //                             Point::new(        //                                 // Line number is 1-based        //                                 range.start.line.saturating_sub(1),        //                                 range.start.col.unwrap_or(0),        //                             )        //                         });        //                         let open_task =        //                             workspace.open_path(project_path, None, true, window, cx);        //                         window        //                             .spawn(cx, async move |cx| {        //                                 let item = open_task.await?;        //                                 if let Some(target) = target {        //                                     if let Some(active_editor) =        //                                         item.downcast::<Editor>()        //                                     {        //                                         active_editor        //                                             .downgrade()        //                                             .update_in(cx, |editor, window, cx| {        //                                                 editor.go_to_singleton_buffer_point(        //                                                     target, window, cx,        //                                                 );        //                                             })        //                                             .log_err();        //                                     }        //                                 }        //                                 anyhow::Ok(())        //                             })        //                             .detach_and_log_err(cx);        //                     }        //                 }        //             })        //             .ok();        //     }        // })        // .into_any_element();         let content = match status {            ToolUseStatus::NeedsConfirmation | ToolUseStatus::Pending | ToolUseStatus::Running => {                div()                    // .child(Label::new(&self.description).size(LabelSize::Small))                    .into_any_element()            }            ToolUseStatus::Finished(str) => {                dbg!(&str);                self.editor.clone().into_any_element()            }            ToolUseStatus::Error(error) => div()                .child(                    Label::new(error.to_string())                        .color(Color::Error)                        .size(LabelSize::Small),                )                .into_any_element(),        };         v_flex()            .my_2()            .border_1()            .border_color(cx.theme().colors().border)            .rounded_sm()            .gap_1()            .child(header)            .child(content)    }} pub struct FindReplaceFileTool; impl Tool for FindReplaceFileTool {    fn name(&self) -> String {        "find_replace_file".into()    }     fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {        false    }     fn description(&self) -> String {        include_str!("find_replace_tool/description.md").to_string()    }     fn icon(&self) -> IconName {        IconName::Pencil    }     fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {        json_schema_for::<FindReplaceFileToolInput>(format)    }     fn ui_text(&self, input: &serde_json::Value) -> String {        match serde_json::from_value::<FindReplaceFileToolInput>(input.clone()) {            Ok(input) => input.display_description,            Err(_) => "Edit file".to_string(),        }    }     fn run(        self: Arc<Self>,        input: serde_json::Value,        _messages: &[LanguageModelRequestMessage],        project: Entity<Project>,        action_log: Entity<ActionLog>,        window: Option<AnyWindowHandle>,        cx: &mut App,    ) -> ToolResult {        let input = match serde_json::from_value::<FindReplaceFileToolInput>(input) {            Ok(input) => input,            Err(err) => return Task::ready(Err(anyhow!(err))).into(),        };        let card = window.and_then(|window| {            window                .update(cx, |_, window, cx| {                    cx.new(|cx| {                        FindReplaceFileToolCard::new(                            input.path.clone(),                            input.display_description.clone(),                            project.clone(),                            window,                            cx,                        )                    })                })                .ok()        });         let output = cx.spawn({            let card = card.clone();            async move |cx: &mut AsyncApp| {            let project_path = project.read_with(cx, |project, cx| {                project                    .find_project_path(&input.path, cx)                    .context("Path not found in project")            })??;             let buffer = project                .update(cx, |project, cx| project.open_buffer(project_path.clone(), cx))?                .await?;             let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;             if input.find.is_empty() {                return Err(anyhow!("`find` string cannot be empty. Use a different tool if you want to create a file."));            }             if input.find == input.replace {                return Err(anyhow!("The `find` and `replace` strings are identical, so no changes would be made."));            }             let result = cx                .background_spawn(async move {                    // Try to match exactly                    let diff = replace_exact(&input.find, &input.replace, &snapshot)

Lines 1–500 of 581.