1use crate::{
2 Templates,
3 edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent},
4 schema::json_schema_for,
5};
6use anyhow::{Context as _, Result, anyhow};
7use assistant_tool::{
8 ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput,
9 ToolUseStatus,
10};
11use language::language_settings::{self, FormatOnSave};
12use project::lsp_store::{FormatTrigger, LspFormatTarget};
13use std::collections::HashSet;
14
15use buffer_diff::{BufferDiff, BufferDiffSnapshot};
16use editor::{Editor, EditorMode, MultiBuffer, PathKey};
17use futures::StreamExt;
18use gpui::{
19 Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, EntityId, Task,
20 TextStyleRefinement, WeakEntity, pulsating_between,
21};
22use indoc::formatdoc;
23use language::{
24 Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Rope, TextBuffer,
25 language_settings::SoftWrap,
26};
27use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
28use markdown::{Markdown, MarkdownElement, MarkdownStyle};
29use project::{Project, ProjectPath};
30use schemars::JsonSchema;
31use serde::{Deserialize, Serialize};
32use settings::Settings;
33use std::{
34 path::{Path, PathBuf},
35 sync::Arc,
36 time::Duration,
37};
38use theme::ThemeSettings;
39use ui::{Disclosure, Tooltip, prelude::*};
40use util::ResultExt;
41use workspace::Workspace;
42
43pub struct EditFileTool;
44
45#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
46pub struct EditFileToolInput {
47 /// A one-line, user-friendly markdown description of the edit. This will be
48 /// shown in the UI and also passed to another model to perform the edit.
49 ///
50 /// Be terse, but also descriptive in what you want to achieve with this
51 /// edit. Avoid generic instructions.
52 ///
53 /// NEVER mention the file path in this description.
54 ///
55 /// <example>Fix API endpoint URLs</example>
56 /// <example>Update copyright year in `page_footer`</example>
57 ///
58 /// Make sure to include this field before all the others in the input object
59 /// so that we can display it immediately.
60 pub display_description: String,
61
62 /// The full path of the file to create or modify in the project.
63 ///
64 /// WARNING: When specifying which file path need changing, you MUST
65 /// start each path with one of the project's root directories.
66 ///
67 /// The following examples assume we have two root directories in the project:
68 /// - backend
69 /// - frontend
70 ///
71 /// <example>
72 /// `backend/src/main.rs`
73 ///
74 /// Notice how the file path starts with root-1. Without that, the path
75 /// would be ambiguous and the call would fail!
76 /// </example>
77 ///
78 /// <example>
79 /// `frontend/db.js`
80 /// </example>
81 pub path: PathBuf,
82
83 /// The mode of operation on the file. Possible values:
84 /// - 'edit': Make granular edits to an existing file.
85 /// - 'create': Create a new file if it doesn't exist.
86 /// - 'overwrite': Replace the entire contents of an existing file.
87 ///
88 /// When a file already exists or you just created it, prefer editing
89 /// it as opposed to recreating it from scratch.
90 pub mode: EditFileMode,
91}
92
93#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
94#[serde(rename_all = "lowercase")]
95pub enum EditFileMode {
96 Edit,
97 Create,
98 Overwrite,
99}
100
101#[derive(Debug, Serialize, Deserialize, JsonSchema)]
102pub struct EditFileToolOutput {
103 pub original_path: PathBuf,
104 pub new_text: String,
105 pub old_text: String,
106 pub raw_output: Option<EditAgentOutput>,
107}
108
109#[derive(Debug, Serialize, Deserialize, JsonSchema)]
110struct PartialInput {
111 #[serde(default)]
112 path: String,
113 #[serde(default)]
114 display_description: String,
115}
116
117const DEFAULT_UI_TEXT: &str = "Editing file";
118
119impl Tool for EditFileTool {
120 fn name(&self) -> String {
121 "edit_file".into()
122 }
123
124 fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
125 false
126 }
127
128 fn description(&self) -> String {
129 include_str!("edit_file_tool/description.md").to_string()
130 }
131
132 fn icon(&self) -> IconName {
133 IconName::Pencil
134 }
135
136 fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
137 json_schema_for::<EditFileToolInput>(format)
138 }
139
140 fn ui_text(&self, input: &serde_json::Value) -> String {
141 match serde_json::from_value::<EditFileToolInput>(input.clone()) {
142 Ok(input) => input.display_description,
143 Err(_) => "Editing file".to_string(),
144 }
145 }
146
147 fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String {
148 if let Some(input) = serde_json::from_value::<PartialInput>(input.clone()).ok() {
149 let description = input.display_description.trim();
150 if !description.is_empty() {
151 return description.to_string();
152 }
153
154 let path = input.path.trim();
155 if !path.is_empty() {
156 return path.to_string();
157 }
158 }
159
160 DEFAULT_UI_TEXT.to_string()
161 }
162
163 fn run(
164 self: Arc<Self>,
165 input: serde_json::Value,
166 request: Arc<LanguageModelRequest>,
167 project: Entity<Project>,
168 action_log: Entity<ActionLog>,
169 model: Arc<dyn LanguageModel>,
170 window: Option<AnyWindowHandle>,
171 cx: &mut App,
172 ) -> ToolResult {
173 let input = match serde_json::from_value::<EditFileToolInput>(input) {
174 Ok(input) => input,
175 Err(err) => return Task::ready(Err(anyhow!(err))).into(),
176 };
177
178 let project_path = match resolve_path(&input, project.clone(), cx) {
179 Ok(path) => path,
180 Err(err) => return Task::ready(Err(anyhow!(err))).into(),
181 };
182
183 let card = window.and_then(|window| {
184 window
185 .update(cx, |_, window, cx| {
186 cx.new(|cx| {
187 EditFileToolCard::new(input.path.clone(), project.clone(), window, cx)
188 })
189 })
190 .ok()
191 });
192
193 let card_clone = card.clone();
194 let task = cx.spawn(async move |cx: &mut AsyncApp| {
195 let edit_agent = EditAgent::new(model, project.clone(), action_log, Templates::new());
196
197 let buffer = project
198 .update(cx, |project, cx| {
199 project.open_buffer(project_path.clone(), cx)
200 })?
201 .await?;
202
203 let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
204 let old_text = cx
205 .background_spawn({
206 let old_snapshot = old_snapshot.clone();
207 async move { old_snapshot.text() }
208 })
209 .await;
210
211 let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) {
212 edit_agent.edit(
213 buffer.clone(),
214 input.display_description.clone(),
215 &request,
216 cx,
217 )
218 } else {
219 edit_agent.overwrite(
220 buffer.clone(),
221 input.display_description.clone(),
222 &request,
223 cx,
224 )
225 };
226
227 let mut hallucinated_old_text = false;
228 while let Some(event) = events.next().await {
229 match event {
230 EditAgentOutputEvent::Edited => {
231 if let Some(card) = card_clone.as_ref() {
232 let new_snapshot =
233 buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
234 let new_text = cx
235 .background_spawn({
236 let new_snapshot = new_snapshot.clone();
237 async move { new_snapshot.text() }
238 })
239 .await;
240 card.update(cx, |card, cx| {
241 card.set_diff(
242 project_path.path.clone(),
243 old_text.clone(),
244 new_text,
245 cx,
246 );
247 })
248 .log_err();
249 }
250 }
251 EditAgentOutputEvent::OldTextNotFound(_) => hallucinated_old_text = true,
252 }
253 }
254 let agent_output = output.await?;
255
256 // Format buffer if format_on_save is enabled, before saving.
257 // If any part of the formatting operation fails, log an error but
258 // don't block the completion of the edit tool's work.
259 let should_format = buffer
260 .read_with(cx, |buffer, cx| {
261 let settings = language_settings::language_settings(
262 buffer.language().map(|l| l.name()),
263 buffer.file(),
264 cx,
265 );
266 !matches!(settings.format_on_save, FormatOnSave::Off)
267 })
268 .log_err()
269 .unwrap_or(false);
270
271 if should_format {
272 let buffers = HashSet::from_iter([buffer.clone()]);
273
274 if let Some(format_task) = project
275 .update(cx, move |project, cx| {
276 project.format(
277 buffers,
278 LspFormatTarget::Buffers,
279 false, // Don't push to history since the tool did it.
280 FormatTrigger::Save,
281 cx,
282 )
283 })
284 .log_err()
285 {
286 format_task.await.log_err();
287 }
288 }
289
290 project
291 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
292 .await?;
293
294 let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
295 let new_text = cx.background_spawn({
296 let new_snapshot = new_snapshot.clone();
297 async move { new_snapshot.text() }
298 });
299 let diff = cx.background_spawn(async move {
300 language::unified_diff(&old_snapshot.text(), &new_snapshot.text())
301 });
302 let (new_text, diff) = futures::join!(new_text, diff);
303
304 let output = EditFileToolOutput {
305 original_path: project_path.path.to_path_buf(),
306 new_text: new_text.clone(),
307 old_text: old_text.clone(),
308 raw_output: Some(agent_output),
309 };
310
311 if let Some(card) = card_clone {
312 card.update(cx, |card, cx| {
313 card.set_diff(project_path.path.clone(), old_text, new_text, cx);
314 })
315 .log_err();
316 }
317
318 let input_path = input.path.display();
319 if diff.is_empty() {
320 anyhow::ensure!(
321 !hallucinated_old_text,
322 formatdoc! {"
323 Some edits were produced but none of them could be applied.
324 Read the relevant sections of {input_path} again so that
325 I can perform the requested edits.
326 "}
327 );
328 Ok("No edits were made.".to_string().into())
329 } else {
330 Ok(ToolResultOutput {
331 content: ToolResultContent::Text(format!(
332 "Edited {}:\n\n```diff\n{}\n```",
333 input_path, diff
334 )),
335 output: serde_json::to_value(output).ok(),
336 })
337 }
338 });
339
340 ToolResult {
341 output: task,
342 card: card.map(AnyToolCard::from),
343 }
344 }
345
346 fn deserialize_card(
347 self: Arc<Self>,
348 output: serde_json::Value,
349 project: Entity<Project>,
350 window: &mut Window,
351 cx: &mut App,
352 ) -> Option<AnyToolCard> {
353 let output = match serde_json::from_value::<EditFileToolOutput>(output) {
354 Ok(output) => output,
355 Err(_) => return None,
356 };
357
358 let card = cx.new(|cx| {
359 let mut card = EditFileToolCard::new(output.original_path.clone(), project, window, cx);
360 card.set_diff(
361 output.original_path.into(),
362 output.old_text,
363 output.new_text,
364 cx,
365 );
366 card
367 });
368
369 Some(card.into())
370 }
371}
372
373/// Validate that the file path is valid, meaning:
374///
375/// - For `edit` and `overwrite`, the path must point to an existing file.
376/// - For `create`, the file must not already exist, but it's parent dir must exist.
377fn resolve_path(
378 input: &EditFileToolInput,
379 project: Entity<Project>,
380 cx: &mut App,
381) -> Result<ProjectPath> {
382 let project = project.read(cx);
383
384 match input.mode {
385 EditFileMode::Edit | EditFileMode::Overwrite => {
386 let path = project
387 .find_project_path(&input.path, cx)
388 .context("Can't edit file: path not found")?;
389
390 let entry = project
391 .entry_for_path(&path, cx)
392 .context("Can't edit file: path not found")?;
393
394 anyhow::ensure!(entry.is_file(), "Can't edit file: path is a directory");
395 Ok(path)
396 }
397
398 EditFileMode::Create => {
399 if let Some(path) = project.find_project_path(&input.path, cx) {
400 anyhow::ensure!(
401 project.entry_for_path(&path, cx).is_none(),
402 "Can't create file: file already exists"
403 );
404 }
405
406 let parent_path = input
407 .path
408 .parent()
409 .context("Can't create file: incorrect path")?;
410
411 let parent_project_path = project.find_project_path(&parent_path, cx);
412
413 let parent_entry = parent_project_path
414 .as_ref()
415 .and_then(|path| project.entry_for_path(&path, cx))
416 .context("Can't create file: parent directory doesn't exist")?;
417
418 anyhow::ensure!(
419 parent_entry.is_dir(),
420 "Can't create file: parent is not a directory"
421 );
422
423 let file_name = input
424 .path
425 .file_name()
426 .context("Can't create file: invalid filename")?;
427
428 let new_file_path = parent_project_path.map(|parent| ProjectPath {
429 path: Arc::from(parent.path.join(file_name)),
430 ..parent
431 });
432
433 new_file_path.context("Can't create file")
434 }
435 }
436}
437
438pub struct EditFileToolCard {
439 path: PathBuf,
440 editor: Entity<Editor>,
441 multibuffer: Entity<MultiBuffer>,
442 project: Entity<Project>,
443 diff_task: Option<Task<Result<()>>>,
444 preview_expanded: bool,
445 error_expanded: Option<Entity<Markdown>>,
446 full_height_expanded: bool,
447 total_lines: Option<u32>,
448 editor_unique_id: EntityId,
449}
450
451impl EditFileToolCard {
452 pub fn new(path: PathBuf, project: Entity<Project>, window: &mut Window, cx: &mut App) -> Self {
453 let multibuffer = cx.new(|_| MultiBuffer::without_headers(Capability::ReadOnly));
454 let editor = cx.new(|cx| {
455 let mut editor = Editor::new(
456 EditorMode::Full {
457 scale_ui_elements_with_buffer_font_size: false,
458 show_active_line_background: false,
459 sized_by_content: true,
460 },
461 multibuffer.clone(),
462 Some(project.clone()),
463 window,
464 cx,
465 );
466 editor.set_show_gutter(false, cx);
467 editor.disable_inline_diagnostics();
468 editor.disable_expand_excerpt_buttons(cx);
469 editor.disable_scrollbars_and_minimap(window, cx);
470 editor.set_soft_wrap_mode(SoftWrap::None, cx);
471 editor.scroll_manager.set_forbid_vertical_scroll(true);
472 editor.set_show_indent_guides(false, cx);
473 editor.set_read_only(true);
474 editor.set_show_breakpoints(false, cx);
475 editor.set_show_code_actions(false, cx);
476 editor.set_show_git_diff_gutter(false, cx);
477 editor.set_expand_all_diff_hunks(cx);
478 editor
479 });
480 Self {
481 editor_unique_id: editor.entity_id(),
482 path,
483 project,
484 editor,
485 multibuffer,
486 diff_task: None,
487 preview_expanded: true,
488 error_expanded: None,
489 full_height_expanded: true,
490 total_lines: None,
491 }
492 }
493
494 pub fn has_diff(&self) -> bool {
495 self.total_lines.is_some()
496 }
497
498 pub fn set_diff(
499 &mut self,
500 path: Arc<Path>,
501 old_text: String,
502 new_text: String,
503 cx: &mut Context<Self>,
504 ) {
505 let language_registry = self.project.read(cx).languages().clone();
506 self.diff_task = Some(cx.spawn(async move |this, cx| {
507 let buffer = build_buffer(new_text, path.clone(), &language_registry, cx).await?;
508 let buffer_diff = build_buffer_diff(old_text, &buffer, &language_registry, cx).await?;
509
510 this.update(cx, |this, cx| {
511 this.total_lines = this.multibuffer.update(cx, |multibuffer, cx| {
512 let snapshot = buffer.read(cx).snapshot();
513 let diff = buffer_diff.read(cx);
514 let diff_hunk_ranges = diff
515 .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)
516 .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
517 .collect::<Vec<_>>();
518 multibuffer.clear(cx);
519 multibuffer.set_excerpts_for_path(
520 PathKey::for_buffer(&buffer, cx),
521 buffer,
522 diff_hunk_ranges,
523 editor::DEFAULT_MULTIBUFFER_CONTEXT,
524 cx,
525 );
526 multibuffer.add_diff(buffer_diff, cx);
527 let end = multibuffer.len(cx);
528 Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1)
529 });
530
531 cx.notify();
532 })
533 }));
534 }
535}
536
537impl ToolCard for EditFileToolCard {
538 fn render(
539 &mut self,
540 status: &ToolUseStatus,
541 window: &mut Window,
542 workspace: WeakEntity<Workspace>,
543 cx: &mut Context<Self>,
544 ) -> impl IntoElement {
545 let error_message = match status {
546 ToolUseStatus::Error(err) => Some(err),
547 _ => None,
548 };
549
550 let path_label_button = h_flex()
551 .id(("edit-tool-path-label-button", self.editor_unique_id))
552 .w_full()
553 .max_w_full()
554 .px_1()
555 .gap_0p5()
556 .cursor_pointer()
557 .rounded_sm()
558 .opacity(0.8)
559 .hover(|label| {
560 label
561 .opacity(1.)
562 .bg(cx.theme().colors().element_hover.opacity(0.5))
563 })
564 .tooltip(Tooltip::text("Jump to File"))
565 .child(
566 h_flex()
567 .child(
568 Icon::new(IconName::Pencil)
569 .size(IconSize::XSmall)
570 .color(Color::Muted),
571 )
572 .child(
573 div()
574 .text_size(rems(0.8125))
575 .child(self.path.display().to_string())
576 .ml_1p5()
577 .mr_0p5(),
578 )
579 .child(
580 Icon::new(IconName::ArrowUpRight)
581 .size(IconSize::XSmall)
582 .color(Color::Ignored),
583 ),
584 )
585 .on_click({
586 let path = self.path.clone();
587 let workspace = workspace.clone();
588 move |_, window, cx| {
589 workspace
590 .update(cx, {
591 |workspace, cx| {
592 let Some(project_path) =
593 workspace.project().read(cx).find_project_path(&path, cx)
594 else {
595 return;
596 };
597 let open_task =
598 workspace.open_path(project_path, None, true, window, cx);
599 window
600 .spawn(cx, async move |cx| {
601 let item = open_task.await?;
602 if let Some(active_editor) = item.downcast::<Editor>() {
603 active_editor
604 .update_in(cx, |editor, window, cx| {
605 editor.go_to_singleton_buffer_point(
606 language::Point::new(0, 0),
607 window,
608 cx,
609 );
610 })
611 .log_err();
612 }
613 anyhow::Ok(())
614 })
615 .detach_and_log_err(cx);
616 }
617 })
618 .ok();
619 }
620 })
621 .into_any_element();
622
623 let codeblock_header_bg = cx
624 .theme()
625 .colors()
626 .element_background
627 .blend(cx.theme().colors().editor_foreground.opacity(0.025));
628
629 let codeblock_header = h_flex()
630 .flex_none()
631 .p_1()
632 .gap_1()
633 .justify_between()
634 .rounded_t_md()
635 .when(error_message.is_none(), |header| {
636 header.bg(codeblock_header_bg)
637 })
638 .child(path_label_button)
639 .when_some(error_message, |header, error_message| {
640 header.child(
641 h_flex()
642 .gap_1()
643 .child(
644 Icon::new(IconName::Close)
645 .size(IconSize::Small)
646 .color(Color::Error),
647 )
648 .child(
649 Disclosure::new(
650 ("edit-file-error-disclosure", self.editor_unique_id),
651 self.error_expanded.is_some(),
652 )
653 .opened_icon(IconName::ChevronUp)
654 .closed_icon(IconName::ChevronDown)
655 .on_click(cx.listener({
656 let error_message = error_message.clone();
657
658 move |this, _event, _window, cx| {
659 if this.error_expanded.is_some() {
660 this.error_expanded.take();
661 } else {
662 this.error_expanded = Some(cx.new(|cx| {
663 Markdown::new(error_message.clone(), None, None, cx)
664 }))
665 }
666 cx.notify();
667 }
668 })),
669 ),
670 )
671 })
672 .when(error_message.is_none() && self.has_diff(), |header| {
673 header.child(
674 Disclosure::new(
675 ("edit-file-disclosure", self.editor_unique_id),
676 self.preview_expanded,
677 )
678 .opened_icon(IconName::ChevronUp)
679 .closed_icon(IconName::ChevronDown)
680 .on_click(cx.listener(
681 move |this, _event, _window, _cx| {
682 this.preview_expanded = !this.preview_expanded;
683 },
684 )),
685 )
686 });
687
688 let (editor, editor_line_height) = self.editor.update(cx, |editor, cx| {
689 let line_height = editor
690 .style()
691 .map(|style| style.text.line_height_in_pixels(window.rem_size()))
692 .unwrap_or_default();
693
694 editor.set_text_style_refinement(TextStyleRefinement {
695 font_size: Some(
696 TextSize::Small
697 .rems(cx)
698 .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
699 .into(),
700 ),
701 ..TextStyleRefinement::default()
702 });
703 let element = editor.render(window, cx);
704 (element.into_any_element(), line_height)
705 });
706
707 let (full_height_icon, full_height_tooltip_label) = if self.full_height_expanded {
708 (IconName::ChevronUp, "Collapse Code Block")
709 } else {
710 (IconName::ChevronDown, "Expand Code Block")
711 };
712
713 let gradient_overlay =
714 div()
715 .absolute()
716 .bottom_0()
717 .left_0()
718 .w_full()
719 .h_2_5()
720 .bg(gpui::linear_gradient(
721 0.,
722 gpui::linear_color_stop(cx.theme().colors().editor_background, 0.),
723 gpui::linear_color_stop(cx.theme().colors().editor_background.opacity(0.), 1.),
724 ));
725
726 let border_color = cx.theme().colors().border.opacity(0.6);
727
728 const DEFAULT_COLLAPSED_LINES: u32 = 10;
729 let is_collapsible = self.total_lines.unwrap_or(0) > DEFAULT_COLLAPSED_LINES;
730
731 let waiting_for_diff = {
732 let styles = [
733 ("w_4_5", (0.1, 0.85), 2000),
734 ("w_1_4", (0.2, 0.75), 2200),
735 ("w_2_4", (0.15, 0.64), 1900),
736 ("w_3_5", (0.25, 0.72), 2300),
737 ("w_2_5", (0.3, 0.56), 1800),
738 ];
739
740 let mut container = v_flex()
741 .p_3()
742 .gap_1()
743 .border_t_1()
744 .rounded_b_md()
745 .border_color(border_color)
746 .bg(cx.theme().colors().editor_background);
747
748 for (width_method, pulse_range, duration_ms) in styles.iter() {
749 let (min_opacity, max_opacity) = *pulse_range;
750 let placeholder = match *width_method {
751 "w_4_5" => div().w_3_4(),
752 "w_1_4" => div().w_1_4(),
753 "w_2_4" => div().w_2_4(),
754 "w_3_5" => div().w_3_5(),
755 "w_2_5" => div().w_2_5(),
756 _ => div().w_1_2(),
757 }
758 .id("loading_div")
759 .h_1()
760 .rounded_full()
761 .bg(cx.theme().colors().element_active)
762 .with_animation(
763 "loading_pulsate",
764 Animation::new(Duration::from_millis(*duration_ms))
765 .repeat()
766 .with_easing(pulsating_between(min_opacity, max_opacity)),
767 |label, delta| label.opacity(delta),
768 );
769
770 container = container.child(placeholder);
771 }
772
773 container
774 };
775
776 v_flex()
777 .mb_2()
778 .border_1()
779 .when(error_message.is_some(), |card| card.border_dashed())
780 .border_color(border_color)
781 .rounded_md()
782 .overflow_hidden()
783 .child(codeblock_header)
784 .when_some(self.error_expanded.as_ref(), |card, error_markdown| {
785 card.child(
786 v_flex()
787 .p_2()
788 .gap_1()
789 .border_t_1()
790 .border_dashed()
791 .border_color(border_color)
792 .bg(cx.theme().colors().editor_background)
793 .rounded_b_md()
794 .child(
795 Label::new("Error")
796 .size(LabelSize::XSmall)
797 .color(Color::Error),
798 )
799 .child(
800 div()
801 .rounded_md()
802 .text_ui_sm(cx)
803 .bg(cx.theme().colors().editor_background)
804 .child(MarkdownElement::new(
805 error_markdown.clone(),
806 markdown_style(window, cx),
807 )),
808 ),
809 )
810 })
811 .when(!self.has_diff() && error_message.is_none(), |card| {
812 card.child(waiting_for_diff)
813 })
814 .when(self.preview_expanded && self.has_diff(), |card| {
815 card.child(
816 v_flex()
817 .relative()
818 .h_full()
819 .when(!self.full_height_expanded, |editor_container| {
820 editor_container
821 .max_h(DEFAULT_COLLAPSED_LINES as f32 * editor_line_height)
822 })
823 .overflow_hidden()
824 .border_t_1()
825 .border_color(border_color)
826 .bg(cx.theme().colors().editor_background)
827 .child(editor)
828 .when(
829 !self.full_height_expanded && is_collapsible,
830 |editor_container| editor_container.child(gradient_overlay),
831 ),
832 )
833 .when(is_collapsible, |card| {
834 card.child(
835 h_flex()
836 .id(("expand-button", self.editor_unique_id))
837 .flex_none()
838 .cursor_pointer()
839 .h_5()
840 .justify_center()
841 .border_t_1()
842 .rounded_b_md()
843 .border_color(border_color)
844 .bg(cx.theme().colors().editor_background)
845 .hover(|style| style.bg(cx.theme().colors().element_hover.opacity(0.1)))
846 .child(
847 Icon::new(full_height_icon)
848 .size(IconSize::Small)
849 .color(Color::Muted),
850 )
851 .tooltip(Tooltip::text(full_height_tooltip_label))
852 .on_click(cx.listener(move |this, _event, _window, _cx| {
853 this.full_height_expanded = !this.full_height_expanded;
854 })),
855 )
856 })
857 })
858 }
859}
860
861fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
862 let theme_settings = ThemeSettings::get_global(cx);
863 let ui_font_size = TextSize::Default.rems(cx);
864 let mut text_style = window.text_style();
865
866 text_style.refine(&TextStyleRefinement {
867 font_family: Some(theme_settings.ui_font.family.clone()),
868 font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
869 font_features: Some(theme_settings.ui_font.features.clone()),
870 font_size: Some(ui_font_size.into()),
871 color: Some(cx.theme().colors().text),
872 ..Default::default()
873 });
874
875 MarkdownStyle {
876 base_text_style: text_style.clone(),
877 selection_background_color: cx.theme().players().local().selection,
878 ..Default::default()
879 }
880}
881
882async fn build_buffer(
883 mut text: String,
884 path: Arc<Path>,
885 language_registry: &Arc<language::LanguageRegistry>,
886 cx: &mut AsyncApp,
887) -> Result<Entity<Buffer>> {
888 let line_ending = LineEnding::detect(&text);
889 LineEnding::normalize(&mut text);
890 let text = Rope::from(text);
891 let language = cx
892 .update(|_cx| language_registry.language_for_file_path(&path))?
893 .await
894 .ok();
895 let buffer = cx.new(|cx| {
896 let buffer = TextBuffer::new_normalized(
897 0,
898 cx.entity_id().as_non_zero_u64().into(),
899 line_ending,
900 text,
901 );
902 let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
903 buffer.set_language(language, cx);
904 buffer
905 })?;
906 Ok(buffer)
907}
908
909async fn build_buffer_diff(
910 mut old_text: String,
911 buffer: &Entity<Buffer>,
912 language_registry: &Arc<LanguageRegistry>,
913 cx: &mut AsyncApp,
914) -> Result<Entity<BufferDiff>> {
915 LineEnding::normalize(&mut old_text);
916
917 let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;
918
919 let base_buffer = cx
920 .update(|cx| {
921 Buffer::build_snapshot(
922 old_text.clone().into(),
923 buffer.language().cloned(),
924 Some(language_registry.clone()),
925 cx,
926 )
927 })?
928 .await;
929
930 let diff_snapshot = cx
931 .update(|cx| {
932 BufferDiffSnapshot::new_with_base_buffer(
933 buffer.text.clone(),
934 Some(old_text.into()),
935 base_buffer,
936 cx,
937 )
938 })?
939 .await;
940
941 let secondary_diff = cx.new(|cx| {
942 let mut diff = BufferDiff::new(&buffer, cx);
943 diff.set_snapshot(diff_snapshot.clone(), &buffer, cx);
944 diff
945 })?;
946
947 cx.new(|cx| {
948 let mut diff = BufferDiff::new(&buffer.text, cx);
949 diff.set_snapshot(diff_snapshot, &buffer, cx);
950 diff.set_secondary_diff(secondary_diff);
951 diff
952 })
953}
954
955#[cfg(test)]
956mod tests {
957 use super::*;
958 use client::TelemetrySettings;
959 use fs::{FakeFs, Fs};
960 use gpui::{TestAppContext, UpdateGlobal};
961 use language::{FakeLspAdapter, Language, LanguageConfig, LanguageMatcher};
962 use language_model::fake_provider::FakeLanguageModel;
963 use language_settings::{AllLanguageSettings, Formatter, FormatterList, SelectedFormatter};
964 use lsp;
965 use serde_json::json;
966 use settings::SettingsStore;
967 use std::sync::Arc;
968 use util::path;
969
970 #[gpui::test]
971 async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
972 init_test(cx);
973
974 let fs = FakeFs::new(cx.executor());
975 fs.insert_tree("/root", json!({})).await;
976 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
977 let action_log = cx.new(|_| ActionLog::new(project.clone()));
978 let model = Arc::new(FakeLanguageModel::default());
979 let result = cx
980 .update(|cx| {
981 let input = serde_json::to_value(EditFileToolInput {
982 display_description: "Some edit".into(),
983 path: "root/nonexistent_file.txt".into(),
984 mode: EditFileMode::Edit,
985 })
986 .unwrap();
987 Arc::new(EditFileTool)
988 .run(
989 input,
990 Arc::default(),
991 project.clone(),
992 action_log,
993 model,
994 None,
995 cx,
996 )
997 .output
998 })
999 .await;
1000 assert_eq!(
1001 result.unwrap_err().to_string(),
1002 "Can't edit file: path not found"
1003 );
1004 }
1005
1006 #[gpui::test]
1007 async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) {
1008 let mode = &EditFileMode::Create;
1009
1010 let result = test_resolve_path(mode, "root/new.txt", cx);
1011 assert_resolved_path_eq(result.await, "new.txt");
1012
1013 let result = test_resolve_path(mode, "new.txt", cx);
1014 assert_resolved_path_eq(result.await, "new.txt");
1015
1016 let result = test_resolve_path(mode, "dir/new.txt", cx);
1017 assert_resolved_path_eq(result.await, "dir/new.txt");
1018
1019 let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx);
1020 assert_eq!(
1021 result.await.unwrap_err().to_string(),
1022 "Can't create file: file already exists"
1023 );
1024
1025 let result = test_resolve_path(mode, "root/dir/nonexistent_dir/new.txt", cx);
1026 assert_eq!(
1027 result.await.unwrap_err().to_string(),
1028 "Can't create file: parent directory doesn't exist"
1029 );
1030 }
1031
1032 #[gpui::test]
1033 async fn test_resolve_path_for_editing_file(cx: &mut TestAppContext) {
1034 let mode = &EditFileMode::Edit;
1035
1036 let path_with_root = "root/dir/subdir/existing.txt";
1037 let path_without_root = "dir/subdir/existing.txt";
1038 let result = test_resolve_path(mode, path_with_root, cx);
1039 assert_resolved_path_eq(result.await, path_without_root);
1040
1041 let result = test_resolve_path(mode, path_without_root, cx);
1042 assert_resolved_path_eq(result.await, path_without_root);
1043
1044 let result = test_resolve_path(mode, "root/nonexistent.txt", cx);
1045 assert_eq!(
1046 result.await.unwrap_err().to_string(),
1047 "Can't edit file: path not found"
1048 );
1049
1050 let result = test_resolve_path(mode, "root/dir", cx);
1051 assert_eq!(
1052 result.await.unwrap_err().to_string(),
1053 "Can't edit file: path is a directory"
1054 );
1055 }
1056
1057 async fn test_resolve_path(
1058 mode: &EditFileMode,
1059 path: &str,
1060 cx: &mut TestAppContext,
1061 ) -> anyhow::Result<ProjectPath> {
1062 init_test(cx);
1063
1064 let fs = FakeFs::new(cx.executor());
1065 fs.insert_tree(
1066 "/root",
1067 json!({
1068 "dir": {
1069 "subdir": {
1070 "existing.txt": "hello"
1071 }
1072 }
1073 }),
1074 )
1075 .await;
1076 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1077
1078 let input = EditFileToolInput {
1079 display_description: "Some edit".into(),
1080 path: path.into(),
1081 mode: mode.clone(),
1082 };
1083
1084 let result = cx.update(|cx| resolve_path(&input, project, cx));
1085 result
1086 }
1087
1088 fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
1089 let actual = path
1090 .expect("Should return valid path")
1091 .path
1092 .to_str()
1093 .unwrap()
1094 .replace("\\", "/"); // Naive Windows paths normalization
1095 assert_eq!(actual, expected);
1096 }
1097
1098 #[test]
1099 fn still_streaming_ui_text_with_path() {
1100 let input = json!({
1101 "path": "src/main.rs",
1102 "display_description": "",
1103 "old_string": "old code",
1104 "new_string": "new code"
1105 });
1106
1107 assert_eq!(EditFileTool.still_streaming_ui_text(&input), "src/main.rs");
1108 }
1109
1110 #[test]
1111 fn still_streaming_ui_text_with_description() {
1112 let input = json!({
1113 "path": "",
1114 "display_description": "Fix error handling",
1115 "old_string": "old code",
1116 "new_string": "new code"
1117 });
1118
1119 assert_eq!(
1120 EditFileTool.still_streaming_ui_text(&input),
1121 "Fix error handling",
1122 );
1123 }
1124
1125 #[test]
1126 fn still_streaming_ui_text_with_path_and_description() {
1127 let input = json!({
1128 "path": "src/main.rs",
1129 "display_description": "Fix error handling",
1130 "old_string": "old code",
1131 "new_string": "new code"
1132 });
1133
1134 assert_eq!(
1135 EditFileTool.still_streaming_ui_text(&input),
1136 "Fix error handling",
1137 );
1138 }
1139
1140 #[test]
1141 fn still_streaming_ui_text_no_path_or_description() {
1142 let input = json!({
1143 "path": "",
1144 "display_description": "",
1145 "old_string": "old code",
1146 "new_string": "new code"
1147 });
1148
1149 assert_eq!(
1150 EditFileTool.still_streaming_ui_text(&input),
1151 DEFAULT_UI_TEXT,
1152 );
1153 }
1154
1155 #[test]
1156 fn still_streaming_ui_text_with_null() {
1157 let input = serde_json::Value::Null;
1158
1159 assert_eq!(
1160 EditFileTool.still_streaming_ui_text(&input),
1161 DEFAULT_UI_TEXT,
1162 );
1163 }
1164
1165 fn init_test(cx: &mut TestAppContext) {
1166 cx.update(|cx| {
1167 let settings_store = SettingsStore::test(cx);
1168 cx.set_global(settings_store);
1169 language::init(cx);
1170 TelemetrySettings::register(cx);
1171 Project::init_settings(cx);
1172 });
1173 }
1174
1175 #[gpui::test]
1176 async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) {
1177 init_test(cx);
1178
1179 let fs = FakeFs::new(cx.executor());
1180 fs.insert_tree("/root", json!({"src": {}})).await;
1181
1182 // Create a simple file with trailing whitespace
1183 fs.save(
1184 path!("/root/src/main.rs").as_ref(),
1185 &"initial content".into(),
1186 LineEnding::Unix,
1187 )
1188 .await
1189 .unwrap();
1190
1191 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1192 let action_log = cx.new(|_| ActionLog::new(project.clone()));
1193 let model = Arc::new(FakeLanguageModel::default());
1194
1195 // First, test with remove_trailing_whitespace_on_save enabled
1196 cx.update(|cx| {
1197 SettingsStore::update_global(cx, |store, cx| {
1198 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1199 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
1200 });
1201 });
1202 });
1203
1204 const CONTENT_WITH_TRAILING_WHITESPACE: &str =
1205 "fn main() { \n println!(\"Hello!\"); \n}\n";
1206
1207 // Have the model stream content that contains trailing whitespace
1208 let edit_result = {
1209 let edit_task = cx.update(|cx| {
1210 let input = serde_json::to_value(EditFileToolInput {
1211 display_description: "Create main function".into(),
1212 path: "root/src/main.rs".into(),
1213 mode: EditFileMode::Overwrite,
1214 })
1215 .unwrap();
1216 Arc::new(EditFileTool)
1217 .run(
1218 input,
1219 Arc::default(),
1220 project.clone(),
1221 action_log.clone(),
1222 model.clone(),
1223 None,
1224 cx,
1225 )
1226 .output
1227 });
1228
1229 // Stream the content with trailing whitespace
1230 cx.executor().run_until_parked();
1231 model.stream_last_completion_response(CONTENT_WITH_TRAILING_WHITESPACE.to_string());
1232 model.end_last_completion_stream();
1233
1234 edit_task.await
1235 };
1236 assert!(edit_result.is_ok());
1237
1238 // Wait for any async operations (e.g. formatting) to complete
1239 cx.executor().run_until_parked();
1240
1241 // Read the file to verify trailing whitespace was removed automatically
1242 assert_eq!(
1243 // Ignore carriage returns on Windows
1244 fs.load(path!("/root/src/main.rs").as_ref())
1245 .await
1246 .unwrap()
1247 .replace("\r\n", "\n"),
1248 "fn main() {\n println!(\"Hello!\");\n}\n",
1249 "Trailing whitespace should be removed when remove_trailing_whitespace_on_save is enabled"
1250 );
1251
1252 // Next, test with remove_trailing_whitespace_on_save disabled
1253 cx.update(|cx| {
1254 SettingsStore::update_global(cx, |store, cx| {
1255 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1256 settings.defaults.remove_trailing_whitespace_on_save = Some(false);
1257 });
1258 });
1259 });
1260
1261 // Stream edits again with trailing whitespace
1262 let edit_result = {
1263 let edit_task = cx.update(|cx| {
1264 let input = serde_json::to_value(EditFileToolInput {
1265 display_description: "Update main function".into(),
1266 path: "root/src/main.rs".into(),
1267 mode: EditFileMode::Overwrite,
1268 })
1269 .unwrap();
1270 Arc::new(EditFileTool)
1271 .run(
1272 input,
1273 Arc::default(),
1274 project.clone(),
1275 action_log.clone(),
1276 model.clone(),
1277 None,
1278 cx,
1279 )
1280 .output
1281 });
1282
1283 // Stream the content with trailing whitespace
1284 cx.executor().run_until_parked();
1285 model.stream_last_completion_response(CONTENT_WITH_TRAILING_WHITESPACE.to_string());
1286 model.end_last_completion_stream();
1287
1288 edit_task.await
1289 };
1290 assert!(edit_result.is_ok());
1291
1292 // Wait for any async operations (e.g. formatting) to complete
1293 cx.executor().run_until_parked();
1294
1295 // Verify the file still has trailing whitespace
1296 // Read the file again - it should still have trailing whitespace
1297 let final_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
1298 assert_eq!(
1299 // Ignore carriage returns on Windows
1300 final_content.replace("\r\n", "\n"),
1301 CONTENT_WITH_TRAILING_WHITESPACE,
1302 "Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled"
1303 );
1304 }
1305
1306 #[gpui::test]
1307 async fn test_format_on_save(cx: &mut TestAppContext) {
1308 init_test(cx);
1309
1310 let fs = FakeFs::new(cx.executor());
1311 fs.insert_tree("/root", json!({"src": {}})).await;
1312
1313 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
1314
1315 // Set up a Rust language with LSP formatting support
1316 let rust_language = Arc::new(Language::new(
1317 LanguageConfig {
1318 name: "Rust".into(),
1319 matcher: LanguageMatcher {
1320 path_suffixes: vec!["rs".to_string()],
1321 ..Default::default()
1322 },
1323 ..Default::default()
1324 },
1325 None,
1326 ));
1327
1328 // Register the language and fake LSP
1329 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1330 language_registry.add(rust_language);
1331
1332 let mut fake_language_servers = language_registry.register_fake_lsp(
1333 "Rust",
1334 FakeLspAdapter {
1335 capabilities: lsp::ServerCapabilities {
1336 document_formatting_provider: Some(lsp::OneOf::Left(true)),
1337 ..Default::default()
1338 },
1339 ..Default::default()
1340 },
1341 );
1342
1343 // Create the file
1344 fs.save(
1345 path!("/root/src/main.rs").as_ref(),
1346 &"initial content".into(),
1347 LineEnding::Unix,
1348 )
1349 .await
1350 .unwrap();
1351
1352 // Open the buffer to trigger LSP initialization
1353 let buffer = project
1354 .update(cx, |project, cx| {
1355 project.open_local_buffer(path!("/root/src/main.rs"), cx)
1356 })
1357 .await
1358 .unwrap();
1359
1360 // Register the buffer with language servers
1361 let _handle = project.update(cx, |project, cx| {
1362 project.register_buffer_with_language_servers(&buffer, cx)
1363 });
1364
1365 const UNFORMATTED_CONTENT: &str = "fn main() {println!(\"Hello!\");}\n";
1366 const FORMATTED_CONTENT: &str =
1367 "This file was formatted by the fake formatter in the test.\n";
1368
1369 // Get the fake language server and set up formatting handler
1370 let fake_language_server = fake_language_servers.next().await.unwrap();
1371 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>({
1372 |_, _| async move {
1373 Ok(Some(vec![lsp::TextEdit {
1374 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(1, 0)),
1375 new_text: FORMATTED_CONTENT.to_string(),
1376 }]))
1377 }
1378 });
1379
1380 let action_log = cx.new(|_| ActionLog::new(project.clone()));
1381 let model = Arc::new(FakeLanguageModel::default());
1382
1383 // First, test with format_on_save enabled
1384 cx.update(|cx| {
1385 SettingsStore::update_global(cx, |store, cx| {
1386 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1387 settings.defaults.format_on_save = Some(FormatOnSave::On);
1388 settings.defaults.formatter = Some(SelectedFormatter::Auto);
1389 });
1390 });
1391 });
1392
1393 // Have the model stream unformatted content
1394 let edit_result = {
1395 let edit_task = cx.update(|cx| {
1396 let input = serde_json::to_value(EditFileToolInput {
1397 display_description: "Create main function".into(),
1398 path: "root/src/main.rs".into(),
1399 mode: EditFileMode::Overwrite,
1400 })
1401 .unwrap();
1402 Arc::new(EditFileTool)
1403 .run(
1404 input,
1405 Arc::default(),
1406 project.clone(),
1407 action_log.clone(),
1408 model.clone(),
1409 None,
1410 cx,
1411 )
1412 .output
1413 });
1414
1415 // Stream the unformatted content
1416 cx.executor().run_until_parked();
1417 model.stream_last_completion_response(UNFORMATTED_CONTENT.to_string());
1418 model.end_last_completion_stream();
1419
1420 edit_task.await
1421 };
1422 assert!(edit_result.is_ok());
1423
1424 // Wait for any async operations (e.g. formatting) to complete
1425 cx.executor().run_until_parked();
1426
1427 // Read the file to verify it was formatted automatically
1428 let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap();
1429 assert_eq!(
1430 // Ignore carriage returns on Windows
1431 new_content.replace("\r\n", "\n"),
1432 FORMATTED_CONTENT,
1433 "Code should be formatted when format_on_save is enabled"
1434 );
1435
1436 // Next, test with format_on_save disabled
1437 cx.update(|cx| {
1438 SettingsStore::update_global(cx, |store, cx| {
1439 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1440 settings.defaults.format_on_save = Some(FormatOnSave::Off);
1441 });
1442 });
1443 });
1444
1445 // Stream unformatted edits again
1446 let edit_result = {
1447 let edit_task = cx.update(|cx| {
1448 let input = serde_json::to_value(EditFileToolInput {
1449 display_description: "Update main function".into(),
1450 path: "root/src/main.rs".into(),
1451 mode: EditFileMode::Overwrite,
1452 })
1453 .unwrap();
1454 Arc::new(EditFileTool)
1455 .run(
1456 input,
1457 Arc::default(),
1458 project.clone(),
1459 action_log.clone(),
1460 model.clone(),
1461 None,
1462 cx,
1463 )
1464 .output
1465 });
1466
1467 // Stream the unformatted content
1468 cx.executor().run_until_parked();
1469 model.stream_last_completion_response(UNFORMATTED_CONTENT.to_string());
1470 model.end_last_completion_stream();
1471
1472 edit_task.await
1473 };
1474 assert!(edit_result.is_ok());
1475
1476 // Wait for any async operations (e.g. formatting) to complete
1477 cx.executor().run_until_parked();
1478
1479 // Verify the file is still unformatted
1480 assert_eq!(
1481 // Ignore carriage returns on Windows
1482 fs.load(path!("/root/src/main.rs").as_ref())
1483 .await
1484 .unwrap()
1485 .replace("\r\n", "\n"),
1486 UNFORMATTED_CONTENT,
1487 "Code should remain unformatted when format_on_save is disabled"
1488 );
1489
1490 // Finally, test with format_on_save set to a list
1491 cx.update(|cx| {
1492 SettingsStore::update_global(cx, |store, cx| {
1493 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1494 settings.defaults.format_on_save = Some(FormatOnSave::List(FormatterList(
1495 vec![Formatter::LanguageServer { name: None }].into(),
1496 )));
1497 });
1498 });
1499 });
1500
1501 // Stream unformatted edits again
1502 let edit_result = {
1503 let edit_task = cx.update(|cx| {
1504 let input = serde_json::to_value(EditFileToolInput {
1505 display_description: "Update main function with list formatter".into(),
1506 path: "root/src/main.rs".into(),
1507 mode: EditFileMode::Overwrite,
1508 })
1509 .unwrap();
1510 Arc::new(EditFileTool)
1511 .run(
1512 input,
1513 Arc::default(),
1514 project.clone(),
1515 action_log.clone(),
1516 model.clone(),
1517 None,
1518 cx,
1519 )
1520 .output
1521 });
1522
1523 // Stream the unformatted content
1524 cx.executor().run_until_parked();
1525 model.stream_last_completion_response(UNFORMATTED_CONTENT.to_string());
1526 model.end_last_completion_stream();
1527
1528 edit_task.await
1529 };
1530 assert!(edit_result.is_ok());
1531
1532 // Wait for any async operations (e.g. formatting) to complete
1533 cx.executor().run_until_parked();
1534
1535 // Read the file to verify it was formatted with the specified formatter
1536 assert_eq!(
1537 // Ignore carriage returns on Windows
1538 fs.load(path!("/root/src/main.rs").as_ref())
1539 .await
1540 .unwrap()
1541 .replace("\r\n", "\n"),
1542 FORMATTED_CONTENT,
1543 "Code should be formatted when format_on_save is set to a list"
1544 );
1545 }
1546}