edit_file_tool.rs

  1use crate::{
  2    Templates,
  3    edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent},
  4    schema::json_schema_for,
  5};
  6use anyhow::{Result, anyhow};
  7use assistant_tool::{
  8    ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput,
  9    ToolUseStatus,
 10};
 11use buffer_diff::{BufferDiff, BufferDiffSnapshot};
 12use editor::{Editor, EditorMode, MultiBuffer, PathKey};
 13use futures::StreamExt;
 14use gpui::{
 15    Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, EntityId, Task,
 16    TextStyleRefinement, WeakEntity, pulsating_between,
 17};
 18use indoc::formatdoc;
 19use language::{
 20    Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Rope, TextBuffer,
 21    language_settings::SoftWrap,
 22};
 23use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
 24use markdown::{Markdown, MarkdownElement, MarkdownStyle};
 25use project::Project;
 26use schemars::JsonSchema;
 27use serde::{Deserialize, Serialize};
 28use settings::Settings;
 29use std::{
 30    path::{Path, PathBuf},
 31    sync::Arc,
 32    time::Duration,
 33};
 34use theme::ThemeSettings;
 35use ui::{Disclosure, Tooltip, prelude::*};
 36use util::ResultExt;
 37use workspace::Workspace;
 38
 39pub struct EditFileTool;
 40
 41#[derive(Debug, Serialize, Deserialize, JsonSchema)]
 42pub struct EditFileToolInput {
 43    /// A one-line, user-friendly markdown description of the edit. This will be
 44    /// shown in the UI and also passed to another model to perform the edit.
 45    ///
 46    /// Be terse, but also descriptive in what you want to achieve with this
 47    /// edit. Avoid generic instructions.
 48    ///
 49    /// NEVER mention the file path in this description.
 50    ///
 51    /// <example>Fix API endpoint URLs</example>
 52    /// <example>Update copyright year in `page_footer`</example>
 53    ///
 54    /// Make sure to include this field before all the others in the input object
 55    /// so that we can display it immediately.
 56    pub display_description: String,
 57
 58    /// The full path of the file to create or modify in the project.
 59    ///
 60    /// WARNING: When specifying which file path need changing, you MUST
 61    /// start each path with one of the project's root directories.
 62    ///
 63    /// The following examples assume we have two root directories in the project:
 64    /// - backend
 65    /// - frontend
 66    ///
 67    /// <example>
 68    /// `backend/src/main.rs`
 69    ///
 70    /// Notice how the file path starts with root-1. Without that, the path
 71    /// would be ambiguous and the call would fail!
 72    /// </example>
 73    ///
 74    /// <example>
 75    /// `frontend/db.js`
 76    /// </example>
 77    pub path: PathBuf,
 78
 79    /// If true, this tool will recreate the file from scratch.
 80    /// If false, this tool will produce granular edits to an existing file.
 81    ///
 82    /// When a file already exists or you just created it, always prefer editing
 83    /// it as opposed to recreating it from scratch.
 84    pub create_or_overwrite: bool,
 85}
 86
 87#[derive(Debug, Serialize, Deserialize, JsonSchema)]
 88pub struct EditFileToolOutput {
 89    pub original_path: PathBuf,
 90    pub new_text: String,
 91    pub old_text: String,
 92    pub raw_output: Option<EditAgentOutput>,
 93}
 94
 95#[derive(Debug, Serialize, Deserialize, JsonSchema)]
 96struct PartialInput {
 97    #[serde(default)]
 98    path: String,
 99    #[serde(default)]
100    display_description: String,
101}
102
103const DEFAULT_UI_TEXT: &str = "Editing file";
104
105impl Tool for EditFileTool {
106    fn name(&self) -> String {
107        "edit_file".into()
108    }
109
110    fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
111        false
112    }
113
114    fn description(&self) -> String {
115        include_str!("edit_file_tool/description.md").to_string()
116    }
117
118    fn icon(&self) -> IconName {
119        IconName::Pencil
120    }
121
122    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
123        json_schema_for::<EditFileToolInput>(format)
124    }
125
126    fn ui_text(&self, input: &serde_json::Value) -> String {
127        match serde_json::from_value::<EditFileToolInput>(input.clone()) {
128            Ok(input) => input.display_description,
129            Err(_) => "Editing file".to_string(),
130        }
131    }
132
133    fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String {
134        if let Some(input) = serde_json::from_value::<PartialInput>(input.clone()).ok() {
135            let description = input.display_description.trim();
136            if !description.is_empty() {
137                return description.to_string();
138            }
139
140            let path = input.path.trim();
141            if !path.is_empty() {
142                return path.to_string();
143            }
144        }
145
146        DEFAULT_UI_TEXT.to_string()
147    }
148
149    fn run(
150        self: Arc<Self>,
151        input: serde_json::Value,
152        request: Arc<LanguageModelRequest>,
153        project: Entity<Project>,
154        action_log: Entity<ActionLog>,
155        model: Arc<dyn LanguageModel>,
156        window: Option<AnyWindowHandle>,
157        cx: &mut App,
158    ) -> ToolResult {
159        let input = match serde_json::from_value::<EditFileToolInput>(input) {
160            Ok(input) => input,
161            Err(err) => return Task::ready(Err(anyhow!(err))).into(),
162        };
163
164        let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
165            return Task::ready(Err(anyhow!(
166                "Path {} not found in project",
167                input.path.display()
168            )))
169            .into();
170        };
171
172        let card = window.and_then(|window| {
173            window
174                .update(cx, |_, window, cx| {
175                    cx.new(|cx| {
176                        EditFileToolCard::new(input.path.clone(), project.clone(), window, cx)
177                    })
178                })
179                .ok()
180        });
181
182        let card_clone = card.clone();
183        let task = cx.spawn(async move |cx: &mut AsyncApp| {
184            let edit_agent = EditAgent::new(model, project.clone(), action_log, Templates::new());
185
186            let buffer = project
187                .update(cx, |project, cx| {
188                    project.open_buffer(project_path.clone(), cx)
189                })?
190                .await?;
191
192            let exists = buffer.read_with(cx, |buffer, _| {
193                buffer
194                    .file()
195                    .as_ref()
196                    .map_or(false, |file| file.disk_state().exists())
197            })?;
198            if !input.create_or_overwrite && !exists {
199                return Err(anyhow!("{} not found", input.path.display()));
200            }
201
202            let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
203            let old_text = cx
204                .background_spawn({
205                    let old_snapshot = old_snapshot.clone();
206                    async move { old_snapshot.text() }
207                })
208                .await;
209
210            let (output, mut events) = if input.create_or_overwrite {
211                edit_agent.overwrite(
212                    buffer.clone(),
213                    input.display_description.clone(),
214                    &request,
215                    cx,
216                )
217            } else {
218                edit_agent.edit(
219                    buffer.clone(),
220                    input.display_description.clone(),
221                    &request,
222                    cx,
223                )
224            };
225
226            let mut hallucinated_old_text = false;
227            while let Some(event) = events.next().await {
228                match event {
229                    EditAgentOutputEvent::Edited => {
230                        if let Some(card) = card_clone.as_ref() {
231                            let new_snapshot =
232                                buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
233                            let new_text = cx
234                                .background_spawn({
235                                    let new_snapshot = new_snapshot.clone();
236                                    async move { new_snapshot.text() }
237                                })
238                                .await;
239                            card.update(cx, |card, cx| {
240                                card.set_diff(
241                                    project_path.path.clone(),
242                                    old_text.clone(),
243                                    new_text,
244                                    cx,
245                                );
246                            })
247                            .log_err();
248                        }
249                    }
250                    EditAgentOutputEvent::OldTextNotFound(_) => hallucinated_old_text = true,
251                }
252            }
253            let agent_output = output.await?;
254
255            project
256                .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
257                .await?;
258
259            let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
260            let new_text = cx.background_spawn({
261                let new_snapshot = new_snapshot.clone();
262                async move { new_snapshot.text() }
263            });
264            let diff = cx.background_spawn(async move {
265                language::unified_diff(&old_snapshot.text(), &new_snapshot.text())
266            });
267            let (new_text, diff) = futures::join!(new_text, diff);
268
269            let output = EditFileToolOutput {
270                original_path: project_path.path.to_path_buf(),
271                new_text: new_text.clone(),
272                old_text: old_text.clone(),
273                raw_output: Some(agent_output),
274            };
275
276            if let Some(card) = card_clone {
277                card.update(cx, |card, cx| {
278                    card.set_diff(project_path.path.clone(), old_text, new_text, cx);
279                })
280                .log_err();
281            }
282
283            let input_path = input.path.display();
284            if diff.is_empty() {
285                if hallucinated_old_text {
286                    Err(anyhow!(formatdoc! {"
287                        Some edits were produced but none of them could be applied.
288                        Read the relevant sections of {input_path} again so that
289                        I can perform the requested edits.
290                    "}))
291                } else {
292                    Ok("No edits were made.".to_string().into())
293                }
294            } else {
295                Ok(ToolResultOutput {
296                    content: ToolResultContent::Text(format!(
297                        "Edited {}:\n\n```diff\n{}\n```",
298                        input_path, diff
299                    )),
300                    output: serde_json::to_value(output).ok(),
301                })
302            }
303        });
304
305        ToolResult {
306            output: task,
307            card: card.map(AnyToolCard::from),
308        }
309    }
310
311    fn deserialize_card(
312        self: Arc<Self>,
313        output: serde_json::Value,
314        project: Entity<Project>,
315        window: &mut Window,
316        cx: &mut App,
317    ) -> Option<AnyToolCard> {
318        let output = match serde_json::from_value::<EditFileToolOutput>(output) {
319            Ok(output) => output,
320            Err(_) => return None,
321        };
322
323        let card = cx.new(|cx| {
324            let mut card = EditFileToolCard::new(output.original_path.clone(), project, window, cx);
325            card.set_diff(
326                output.original_path.into(),
327                output.old_text,
328                output.new_text,
329                cx,
330            );
331            card
332        });
333
334        Some(card.into())
335    }
336}
337
338pub struct EditFileToolCard {
339    path: PathBuf,
340    editor: Entity<Editor>,
341    multibuffer: Entity<MultiBuffer>,
342    project: Entity<Project>,
343    diff_task: Option<Task<Result<()>>>,
344    preview_expanded: bool,
345    error_expanded: Option<Entity<Markdown>>,
346    full_height_expanded: bool,
347    total_lines: Option<u32>,
348    editor_unique_id: EntityId,
349}
350
351impl EditFileToolCard {
352    pub fn new(path: PathBuf, project: Entity<Project>, window: &mut Window, cx: &mut App) -> Self {
353        let multibuffer = cx.new(|_| MultiBuffer::without_headers(Capability::ReadOnly));
354        let editor = cx.new(|cx| {
355            let mut editor = Editor::new(
356                EditorMode::Full {
357                    scale_ui_elements_with_buffer_font_size: false,
358                    show_active_line_background: false,
359                    sized_by_content: true,
360                },
361                multibuffer.clone(),
362                Some(project.clone()),
363                window,
364                cx,
365            );
366            editor.set_show_gutter(false, cx);
367            editor.disable_inline_diagnostics();
368            editor.disable_expand_excerpt_buttons(cx);
369            editor.disable_scrollbars_and_minimap(window, cx);
370            editor.set_soft_wrap_mode(SoftWrap::None, cx);
371            editor.scroll_manager.set_forbid_vertical_scroll(true);
372            editor.set_show_indent_guides(false, cx);
373            editor.set_read_only(true);
374            editor.set_show_breakpoints(false, cx);
375            editor.set_show_code_actions(false, cx);
376            editor.set_show_git_diff_gutter(false, cx);
377            editor.set_expand_all_diff_hunks(cx);
378            editor
379        });
380        Self {
381            editor_unique_id: editor.entity_id(),
382            path,
383            project,
384            editor,
385            multibuffer,
386            diff_task: None,
387            preview_expanded: true,
388            error_expanded: None,
389            full_height_expanded: false,
390            total_lines: None,
391        }
392    }
393
394    pub fn has_diff(&self) -> bool {
395        self.total_lines.is_some()
396    }
397
398    pub fn set_diff(
399        &mut self,
400        path: Arc<Path>,
401        old_text: String,
402        new_text: String,
403        cx: &mut Context<Self>,
404    ) {
405        let language_registry = self.project.read(cx).languages().clone();
406        self.diff_task = Some(cx.spawn(async move |this, cx| {
407            let buffer = build_buffer(new_text, path.clone(), &language_registry, cx).await?;
408            let buffer_diff = build_buffer_diff(old_text, &buffer, &language_registry, cx).await?;
409
410            this.update(cx, |this, cx| {
411                this.total_lines = this.multibuffer.update(cx, |multibuffer, cx| {
412                    let snapshot = buffer.read(cx).snapshot();
413                    let diff = buffer_diff.read(cx);
414                    let diff_hunk_ranges = diff
415                        .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)
416                        .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
417                        .collect::<Vec<_>>();
418                    multibuffer.clear(cx);
419                    multibuffer.set_excerpts_for_path(
420                        PathKey::for_buffer(&buffer, cx),
421                        buffer,
422                        diff_hunk_ranges,
423                        editor::DEFAULT_MULTIBUFFER_CONTEXT,
424                        cx,
425                    );
426                    multibuffer.add_diff(buffer_diff, cx);
427                    let end = multibuffer.len(cx);
428                    Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1)
429                });
430
431                cx.notify();
432            })
433        }));
434    }
435}
436
437impl ToolCard for EditFileToolCard {
438    fn render(
439        &mut self,
440        status: &ToolUseStatus,
441        window: &mut Window,
442        workspace: WeakEntity<Workspace>,
443        cx: &mut Context<Self>,
444    ) -> impl IntoElement {
445        let error_message = match status {
446            ToolUseStatus::Error(err) => Some(err),
447            _ => None,
448        };
449
450        let path_label_button = h_flex()
451            .id(("edit-tool-path-label-button", self.editor_unique_id))
452            .w_full()
453            .max_w_full()
454            .px_1()
455            .gap_0p5()
456            .cursor_pointer()
457            .rounded_sm()
458            .opacity(0.8)
459            .hover(|label| {
460                label
461                    .opacity(1.)
462                    .bg(cx.theme().colors().element_hover.opacity(0.5))
463            })
464            .tooltip(Tooltip::text("Jump to File"))
465            .child(
466                h_flex()
467                    .child(
468                        Icon::new(IconName::Pencil)
469                            .size(IconSize::XSmall)
470                            .color(Color::Muted),
471                    )
472                    .child(
473                        div()
474                            .text_size(rems(0.8125))
475                            .child(self.path.display().to_string())
476                            .ml_1p5()
477                            .mr_0p5(),
478                    )
479                    .child(
480                        Icon::new(IconName::ArrowUpRight)
481                            .size(IconSize::XSmall)
482                            .color(Color::Ignored),
483                    ),
484            )
485            .on_click({
486                let path = self.path.clone();
487                let workspace = workspace.clone();
488                move |_, window, cx| {
489                    workspace
490                        .update(cx, {
491                            |workspace, cx| {
492                                let Some(project_path) =
493                                    workspace.project().read(cx).find_project_path(&path, cx)
494                                else {
495                                    return;
496                                };
497                                let open_task =
498                                    workspace.open_path(project_path, None, true, window, cx);
499                                window
500                                    .spawn(cx, async move |cx| {
501                                        let item = open_task.await?;
502                                        if let Some(active_editor) = item.downcast::<Editor>() {
503                                            active_editor
504                                                .update_in(cx, |editor, window, cx| {
505                                                    editor.go_to_singleton_buffer_point(
506                                                        language::Point::new(0, 0),
507                                                        window,
508                                                        cx,
509                                                    );
510                                                })
511                                                .log_err();
512                                        }
513                                        anyhow::Ok(())
514                                    })
515                                    .detach_and_log_err(cx);
516                            }
517                        })
518                        .ok();
519                }
520            })
521            .into_any_element();
522
523        let codeblock_header_bg = cx
524            .theme()
525            .colors()
526            .element_background
527            .blend(cx.theme().colors().editor_foreground.opacity(0.025));
528
529        let codeblock_header = h_flex()
530            .flex_none()
531            .p_1()
532            .gap_1()
533            .justify_between()
534            .rounded_t_md()
535            .when(error_message.is_none(), |header| {
536                header.bg(codeblock_header_bg)
537            })
538            .child(path_label_button)
539            .when_some(error_message, |header, error_message| {
540                header.child(
541                    h_flex()
542                        .gap_1()
543                        .child(
544                            Icon::new(IconName::Close)
545                                .size(IconSize::Small)
546                                .color(Color::Error),
547                        )
548                        .child(
549                            Disclosure::new(
550                                ("edit-file-error-disclosure", self.editor_unique_id),
551                                self.error_expanded.is_some(),
552                            )
553                            .opened_icon(IconName::ChevronUp)
554                            .closed_icon(IconName::ChevronDown)
555                            .on_click(cx.listener({
556                                let error_message = error_message.clone();
557
558                                move |this, _event, _window, cx| {
559                                    if this.error_expanded.is_some() {
560                                        this.error_expanded.take();
561                                    } else {
562                                        this.error_expanded = Some(cx.new(|cx| {
563                                            Markdown::new(error_message.clone(), None, None, cx)
564                                        }))
565                                    }
566                                    cx.notify();
567                                }
568                            })),
569                        ),
570                )
571            })
572            .when(error_message.is_none() && self.has_diff(), |header| {
573                header.child(
574                    Disclosure::new(
575                        ("edit-file-disclosure", self.editor_unique_id),
576                        self.preview_expanded,
577                    )
578                    .opened_icon(IconName::ChevronUp)
579                    .closed_icon(IconName::ChevronDown)
580                    .on_click(cx.listener(
581                        move |this, _event, _window, _cx| {
582                            this.preview_expanded = !this.preview_expanded;
583                        },
584                    )),
585                )
586            });
587
588        let (editor, editor_line_height) = self.editor.update(cx, |editor, cx| {
589            let line_height = editor
590                .style()
591                .map(|style| style.text.line_height_in_pixels(window.rem_size()))
592                .unwrap_or_default();
593
594            editor.set_text_style_refinement(TextStyleRefinement {
595                font_size: Some(
596                    TextSize::Small
597                        .rems(cx)
598                        .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
599                        .into(),
600                ),
601                ..TextStyleRefinement::default()
602            });
603            let element = editor.render(window, cx);
604            (element.into_any_element(), line_height)
605        });
606
607        let (full_height_icon, full_height_tooltip_label) = if self.full_height_expanded {
608            (IconName::ChevronUp, "Collapse Code Block")
609        } else {
610            (IconName::ChevronDown, "Expand Code Block")
611        };
612
613        let gradient_overlay =
614            div()
615                .absolute()
616                .bottom_0()
617                .left_0()
618                .w_full()
619                .h_2_5()
620                .bg(gpui::linear_gradient(
621                    0.,
622                    gpui::linear_color_stop(cx.theme().colors().editor_background, 0.),
623                    gpui::linear_color_stop(cx.theme().colors().editor_background.opacity(0.), 1.),
624                ));
625
626        let border_color = cx.theme().colors().border.opacity(0.6);
627
628        const DEFAULT_COLLAPSED_LINES: u32 = 10;
629        let is_collapsible = self.total_lines.unwrap_or(0) > DEFAULT_COLLAPSED_LINES;
630
631        let waiting_for_diff = {
632            let styles = [
633                ("w_4_5", (0.1, 0.85), 2000),
634                ("w_1_4", (0.2, 0.75), 2200),
635                ("w_2_4", (0.15, 0.64), 1900),
636                ("w_3_5", (0.25, 0.72), 2300),
637                ("w_2_5", (0.3, 0.56), 1800),
638            ];
639
640            let mut container = v_flex()
641                .p_3()
642                .gap_1()
643                .border_t_1()
644                .rounded_b_md()
645                .border_color(border_color)
646                .bg(cx.theme().colors().editor_background);
647
648            for (width_method, pulse_range, duration_ms) in styles.iter() {
649                let (min_opacity, max_opacity) = *pulse_range;
650                let placeholder = match *width_method {
651                    "w_4_5" => div().w_3_4(),
652                    "w_1_4" => div().w_1_4(),
653                    "w_2_4" => div().w_2_4(),
654                    "w_3_5" => div().w_3_5(),
655                    "w_2_5" => div().w_2_5(),
656                    _ => div().w_1_2(),
657                }
658                .id("loading_div")
659                .h_1()
660                .rounded_full()
661                .bg(cx.theme().colors().element_active)
662                .with_animation(
663                    "loading_pulsate",
664                    Animation::new(Duration::from_millis(*duration_ms))
665                        .repeat()
666                        .with_easing(pulsating_between(min_opacity, max_opacity)),
667                    |label, delta| label.opacity(delta),
668                );
669
670                container = container.child(placeholder);
671            }
672
673            container
674        };
675
676        v_flex()
677            .mb_2()
678            .border_1()
679            .when(error_message.is_some(), |card| card.border_dashed())
680            .border_color(border_color)
681            .rounded_md()
682            .overflow_hidden()
683            .child(codeblock_header)
684            .when_some(self.error_expanded.as_ref(), |card, error_markdown| {
685                card.child(
686                    v_flex()
687                        .p_2()
688                        .gap_1()
689                        .border_t_1()
690                        .border_dashed()
691                        .border_color(border_color)
692                        .bg(cx.theme().colors().editor_background)
693                        .rounded_b_md()
694                        .child(
695                            Label::new("Error")
696                                .size(LabelSize::XSmall)
697                                .color(Color::Error),
698                        )
699                        .child(
700                            div()
701                                .rounded_md()
702                                .text_ui_sm(cx)
703                                .bg(cx.theme().colors().editor_background)
704                                .child(MarkdownElement::new(
705                                    error_markdown.clone(),
706                                    markdown_style(window, cx),
707                                )),
708                        ),
709                )
710            })
711            .when(!self.has_diff() && error_message.is_none(), |card| {
712                card.child(waiting_for_diff)
713            })
714            .when(self.preview_expanded && self.has_diff(), |card| {
715                card.child(
716                    v_flex()
717                        .relative()
718                        .h_full()
719                        .when(!self.full_height_expanded, |editor_container| {
720                            editor_container
721                                .max_h(DEFAULT_COLLAPSED_LINES as f32 * editor_line_height)
722                        })
723                        .overflow_hidden()
724                        .border_t_1()
725                        .border_color(border_color)
726                        .bg(cx.theme().colors().editor_background)
727                        .child(editor)
728                        .when(
729                            !self.full_height_expanded && is_collapsible,
730                            |editor_container| editor_container.child(gradient_overlay),
731                        ),
732                )
733                .when(is_collapsible, |card| {
734                    card.child(
735                        h_flex()
736                            .id(("expand-button", self.editor_unique_id))
737                            .flex_none()
738                            .cursor_pointer()
739                            .h_5()
740                            .justify_center()
741                            .border_t_1()
742                            .rounded_b_md()
743                            .border_color(border_color)
744                            .bg(cx.theme().colors().editor_background)
745                            .hover(|style| style.bg(cx.theme().colors().element_hover.opacity(0.1)))
746                            .child(
747                                Icon::new(full_height_icon)
748                                    .size(IconSize::Small)
749                                    .color(Color::Muted),
750                            )
751                            .tooltip(Tooltip::text(full_height_tooltip_label))
752                            .on_click(cx.listener(move |this, _event, _window, _cx| {
753                                this.full_height_expanded = !this.full_height_expanded;
754                            })),
755                    )
756                })
757            })
758    }
759}
760
761fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
762    let theme_settings = ThemeSettings::get_global(cx);
763    let ui_font_size = TextSize::Default.rems(cx);
764    let mut text_style = window.text_style();
765
766    text_style.refine(&TextStyleRefinement {
767        font_family: Some(theme_settings.ui_font.family.clone()),
768        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
769        font_features: Some(theme_settings.ui_font.features.clone()),
770        font_size: Some(ui_font_size.into()),
771        color: Some(cx.theme().colors().text),
772        ..Default::default()
773    });
774
775    MarkdownStyle {
776        base_text_style: text_style.clone(),
777        selection_background_color: cx.theme().players().local().selection,
778        ..Default::default()
779    }
780}
781
782async fn build_buffer(
783    mut text: String,
784    path: Arc<Path>,
785    language_registry: &Arc<language::LanguageRegistry>,
786    cx: &mut AsyncApp,
787) -> Result<Entity<Buffer>> {
788    let line_ending = LineEnding::detect(&text);
789    LineEnding::normalize(&mut text);
790    let text = Rope::from(text);
791    let language = cx
792        .update(|_cx| language_registry.language_for_file_path(&path))?
793        .await
794        .ok();
795    let buffer = cx.new(|cx| {
796        let buffer = TextBuffer::new_normalized(
797            0,
798            cx.entity_id().as_non_zero_u64().into(),
799            line_ending,
800            text,
801        );
802        let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
803        buffer.set_language(language, cx);
804        buffer
805    })?;
806    Ok(buffer)
807}
808
809async fn build_buffer_diff(
810    mut old_text: String,
811    buffer: &Entity<Buffer>,
812    language_registry: &Arc<LanguageRegistry>,
813    cx: &mut AsyncApp,
814) -> Result<Entity<BufferDiff>> {
815    LineEnding::normalize(&mut old_text);
816
817    let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;
818
819    let base_buffer = cx
820        .update(|cx| {
821            Buffer::build_snapshot(
822                old_text.clone().into(),
823                buffer.language().cloned(),
824                Some(language_registry.clone()),
825                cx,
826            )
827        })?
828        .await;
829
830    let diff_snapshot = cx
831        .update(|cx| {
832            BufferDiffSnapshot::new_with_base_buffer(
833                buffer.text.clone(),
834                Some(old_text.into()),
835                base_buffer,
836                cx,
837            )
838        })?
839        .await;
840
841    let secondary_diff = cx.new(|cx| {
842        let mut diff = BufferDiff::new(&buffer, cx);
843        diff.set_snapshot(diff_snapshot.clone(), &buffer, cx);
844        diff
845    })?;
846
847    cx.new(|cx| {
848        let mut diff = BufferDiff::new(&buffer.text, cx);
849        diff.set_snapshot(diff_snapshot, &buffer, cx);
850        diff.set_secondary_diff(secondary_diff);
851        diff
852    })
853}
854
855#[cfg(test)]
856mod tests {
857    use super::*;
858    use fs::FakeFs;
859    use gpui::TestAppContext;
860    use language_model::fake_provider::FakeLanguageModel;
861    use serde_json::json;
862    use settings::SettingsStore;
863    use util::path;
864
865    #[gpui::test]
866    async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
867        init_test(cx);
868
869        let fs = FakeFs::new(cx.executor());
870        fs.insert_tree("/root", json!({})).await;
871        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
872        let action_log = cx.new(|_| ActionLog::new(project.clone()));
873        let model = Arc::new(FakeLanguageModel::default());
874        let result = cx
875            .update(|cx| {
876                let input = serde_json::to_value(EditFileToolInput {
877                    display_description: "Some edit".into(),
878                    path: "root/nonexistent_file.txt".into(),
879                    create_or_overwrite: false,
880                })
881                .unwrap();
882                Arc::new(EditFileTool)
883                    .run(
884                        input,
885                        Arc::default(),
886                        project.clone(),
887                        action_log,
888                        model,
889                        None,
890                        cx,
891                    )
892                    .output
893            })
894            .await;
895        assert_eq!(
896            result.unwrap_err().to_string(),
897            "root/nonexistent_file.txt not found"
898        );
899    }
900
901    #[test]
902    fn still_streaming_ui_text_with_path() {
903        let input = json!({
904            "path": "src/main.rs",
905            "display_description": "",
906            "old_string": "old code",
907            "new_string": "new code"
908        });
909
910        assert_eq!(EditFileTool.still_streaming_ui_text(&input), "src/main.rs");
911    }
912
913    #[test]
914    fn still_streaming_ui_text_with_description() {
915        let input = json!({
916            "path": "",
917            "display_description": "Fix error handling",
918            "old_string": "old code",
919            "new_string": "new code"
920        });
921
922        assert_eq!(
923            EditFileTool.still_streaming_ui_text(&input),
924            "Fix error handling",
925        );
926    }
927
928    #[test]
929    fn still_streaming_ui_text_with_path_and_description() {
930        let input = json!({
931            "path": "src/main.rs",
932            "display_description": "Fix error handling",
933            "old_string": "old code",
934            "new_string": "new code"
935        });
936
937        assert_eq!(
938            EditFileTool.still_streaming_ui_text(&input),
939            "Fix error handling",
940        );
941    }
942
943    #[test]
944    fn still_streaming_ui_text_no_path_or_description() {
945        let input = json!({
946            "path": "",
947            "display_description": "",
948            "old_string": "old code",
949            "new_string": "new code"
950        });
951
952        assert_eq!(
953            EditFileTool.still_streaming_ui_text(&input),
954            DEFAULT_UI_TEXT,
955        );
956    }
957
958    #[test]
959    fn still_streaming_ui_text_with_null() {
960        let input = serde_json::Value::Null;
961
962        assert_eq!(
963            EditFileTool.still_streaming_ui_text(&input),
964            DEFAULT_UI_TEXT,
965        );
966    }
967
968    fn init_test(cx: &mut TestAppContext) {
969        cx.update(|cx| {
970            let settings_store = SettingsStore::test(cx);
971            cx.set_global(settings_store);
972            language::init(cx);
973            Project::init_settings(cx);
974        });
975    }
976}