use crate::{ Templates, edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat}, schema::json_schema_for, ui::{COLLAPSED_LINES, ToolOutputPreview},};use anyhow::{Context as _, Result, anyhow};use assistant_tool::{ ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,};use buffer_diff::{BufferDiff, BufferDiffSnapshot};use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, scroll::Autoscroll};use futures::StreamExt;use gpui::{ Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task, TextStyleRefinement, WeakEntity, pulsating_between, px,};use indoc::formatdoc;use language::{ Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Point, Rope, TextBuffer, language_settings::{self, FormatOnSave, SoftWrap},};use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};use markdown::{Markdown, MarkdownElement, MarkdownStyle};use project::{ Project, ProjectPath, lsp_store::{FormatTrigger, LspFormatTarget},};use schemars::JsonSchema;use serde::{Deserialize, Serialize};use settings::Settings;use std::{ cmp::Reverse, collections::HashSet, ops::Range, path::{Path, PathBuf}, sync::Arc, time::Duration,};use theme::ThemeSettings;use ui::{Disclosure, Tooltip, prelude::*};use util::ResultExt;use workspace::Workspace; pub struct EditFileTool; #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]pub struct EditFileToolInput { /// A one-line, user-friendly markdown description of the edit. This will be /// shown in the UI and also passed to another model to perform the edit. /// /// Be terse, but also descriptive in what you want to achieve with this /// edit. Avoid generic instructions. /// /// NEVER mention the file path in this description. /// /// <example>Fix API endpoint URLs</example> /// <example>Update copyright year in `page_footer`</example> /// /// Make sure to include this field before all the others in the input object /// so that we can display it immediately. pub display_description: String, /// The full path of the file to create or modify in the project. /// /// WARNING: When specifying which file path need changing, you MUST /// start each path with one of the project's root directories. /// /// The following examples assume we have two root directories in the project: /// - /a/b/backend /// - /c/d/frontend /// /// <example> /// `backend/src/main.rs` /// /// Notice how the file path starts with `backend`. Without that, the path /// would be ambiguous and the call would fail! /// </example> /// /// <example> /// `frontend/db.js` /// </example> pub path: PathBuf, /// The mode of operation on the file. Possible values: /// - 'edit': Make granular edits to an existing file. /// - 'create': Create a new file if it doesn't exist. /// - 'overwrite': Replace the entire contents of an existing file. /// /// When a file already exists or you just created it, prefer editing /// it as opposed to recreating it from scratch. pub mode: EditFileMode,} #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]#[serde(rename_all = "lowercase")]pub enum EditFileMode { Edit, Create, Overwrite,} #[derive(Debug, Serialize, Deserialize, JsonSchema)]pub struct EditFileToolOutput { pub original_path: PathBuf, pub new_text: String, pub old_text: Arc<String>, pub raw_output: Option<EditAgentOutput>,} #[derive(Debug, Serialize, Deserialize, JsonSchema)]struct PartialInput { #[serde(default)] path: String, #[serde(default)] display_description: String,} const DEFAULT_UI_TEXT: &str = "Editing file"; impl Tool for EditFileTool { fn name(&self) -> String { "edit_file".into() } fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { false } fn may_perform_edits(&self) -> bool { true } fn description(&self) -> String { include_str!("edit_file_tool/description.md").to_string() } fn icon(&self) -> IconName { IconName::Pencil } fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> { json_schema_for::<EditFileToolInput>(format) } fn ui_text(&self, input: &serde_json::Value) -> String { match serde_json::from_value::<EditFileToolInput>(input.clone()) { Ok(input) => input.display_description, Err(_) => "Editing file".to_string(), } } fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String { if let Some(input) = serde_json::from_value::<PartialInput>(input.clone()).ok() { let description = input.display_description.trim(); if !description.is_empty() { return description.to_string(); } let path = input.path.trim(); if !path.is_empty() { return path.to_string(); } } DEFAULT_UI_TEXT.to_string() } fn run( self: Arc<Self>, input: serde_json::Value, request: Arc<LanguageModelRequest>, project: Entity<Project>, action_log: Entity<ActionLog>, model: Arc<dyn LanguageModel>, window: Option<AnyWindowHandle>, cx: &mut App, ) -> ToolResult { let input = match serde_json::from_value::<EditFileToolInput>(input) { Ok(input) => input, Err(err) => return Task::ready(Err(anyhow!(err))).into(), }; let project_path = match resolve_path(&input, project.clone(), cx) { Ok(path) => path, Err(err) => return Task::ready(Err(anyhow!(err))).into(), }; let card = window.and_then(|window| { window .update(cx, |_, window, cx| { cx.new(|cx| { EditFileToolCard::new(input.path.clone(), project.clone(), window, cx) }) }) .ok() }); let card_clone = card.clone(); let action_log_clone = action_log.clone(); let task = cx.spawn(async move |cx: &mut AsyncApp| { let edit_format = EditFormat::from_model(model.clone())?; let edit_agent = EditAgent::new( model, project.clone(), action_log_clone, Templates::new(), edit_format, ); let buffer = project .update(cx, |project, cx| { project.open_buffer(project_path.clone(), cx) })? .await?; let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; let old_text = cx .background_spawn({ let old_snapshot = old_snapshot.clone(); async move { Arc::new(old_snapshot.text()) } }) .await; if let Some(card) = card_clone.as_ref() { card.update(cx, |card, cx| card.initialize(buffer.clone(), cx))?; } let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) { edit_agent.edit( buffer.clone(), input.display_description.clone(), &request, cx, ) } else { edit_agent.overwrite( buffer.clone(), input.display_description.clone(), &request, cx, ) }; let mut hallucinated_old_text = false; let mut ambiguous_ranges = Vec::new(); while let Some(event) = events.next().await { match event { EditAgentOutputEvent::Edited => { if let Some(card) = card_clone.as_ref() { card.update(cx, |card, cx| card.update_diff(cx))?; } } EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true, EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges, EditAgentOutputEvent::ResolvingEditRange(range) => { if let Some(card) = card_clone.as_ref() { card.update(cx, |card, cx| card.reveal_range(range, cx))?; } } } } let agent_output = output.await?; // If format_on_save is enabled, format the buffer let format_on_save_enabled = buffer .read_with(cx, |buffer, cx| { let settings = language_settings::language_settings( buffer.language().map(|l| l.name()), buffer.file(), cx, ); !matches!(settings.format_on_save, FormatOnSave::Off) }) .unwrap_or(false); if format_on_save_enabled { let format_task = project.update(cx, |project, cx| { project.format( HashSet::from_iter([buffer.clone()]), LspFormatTarget::Buffers, false, // Don't push to history since the tool did it. FormatTrigger::Save, cx, ) })?; format_task.await.log_err(); } project .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))? .await?; // Notify the action log that we've edited the buffer (*after* formatting has completed). action_log.update(cx, |log, cx| { log.buffer_edited(buffer.clone(), cx); })?; let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; let (new_text, diff) = cx .background_spawn({ let new_snapshot = new_snapshot.clone(); let old_text = old_text.clone(); async move { let new_text = new_snapshot.text(); let diff = language::unified_diff(&old_text, &new_text); (new_text, diff) } }) .await; let output = EditFileToolOutput { original_path: project_path.path.to_path_buf(), new_text: new_text.clone(), old_text, raw_output: Some(agent_output), }; if let Some(card) = card_clone { card.update(cx, |card, cx| { card.update_diff(cx); card.finalize(cx) }) .log_err(); } let input_path = input.path.display(); if diff.is_empty() { anyhow::ensure!( !hallucinated_old_text, formatdoc! {" Some edits were produced but none of them could be applied. Read the relevant sections of {input_path} again so that I can perform the requested edits. "} ); anyhow::ensure!( ambiguous_ranges.is_empty(), { let line_numbers = ambiguous_ranges .iter() .map(|range| range.start.to_string()) .collect::<Vec<_>>() .join(", "); formatdoc! {" <old_text> matches more than one position in the file (lines: {line_numbers}). Read the relevant sections of {input_path} again and extend <old_text> so that I can perform the requested edits. "} } ); Ok(ToolResultOutput { content: ToolResultContent::Text("No edits were made.".into()), output: serde_json::to_value(output).ok(), }) } else { Ok(ToolResultOutput { content: ToolResultContent::Text(format!( "Edited {}:\n\n```diff\n{}\n```", input_path, diff )), output: serde_json::to_value(output).ok(), }) } }); ToolResult { output: task, card: card.map(AnyToolCard::from), } } fn deserialize_card( self: Arc<Self>, output: serde_json::Value, project: Entity<Project>, window: &mut Window, cx: &mut App, ) -> Option<AnyToolCard> { let output = match serde_json::from_value::<EditFileToolOutput>(output) { Ok(output) => output, Err(_) => return None, }; let card = cx.new(|cx| { EditFileToolCard::new(output.original_path.clone(), project.clone(), window, cx) }); cx.spawn({ let path: Arc<Path> = output.original_path.into(); let language_registry = project.read(cx).languages().clone(); let card = card.clone(); async move |cx| { let buffer = build_buffer(output.new_text, path.clone(), &language_registry, cx).await?; let buffer_diff = build_buffer_diff(output.old_text.clone(), &buffer, &language_registry, cx) .await?; card.update(cx, |card, cx| { card.multibuffer.update(cx, |multibuffer, cx| { let snapshot = buffer.read(cx).snapshot(); let diff = buffer_diff.read(cx); let diff_hunk_ranges = diff .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx) .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot)) .collect::<Vec<_>>(); multibuffer.set_excerpts_for_path( PathKey::for_buffer(&buffer, cx), buffer, diff_hunk_ranges, editor::DEFAULT_MULTIBUFFER_CONTEXT, cx, ); multibuffer.add_diff(buffer_diff, cx); let end = multibuffer.len(cx); card.total_lines = Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1); }); cx.notify(); })?; anyhow::Ok(()) } }) .detach_and_log_err(cx); Some(card.into()) }} /// Validate that the file path is valid, meaning:////// - For `edit` and `overwrite`, the path must point to an existing file./// - For `create`, the file must not already exist, but it's parent dir must exist.fn resolve_path( input: &EditFileToolInput, project: Entity<Project>, cx: &mut App,) -> Result<ProjectPath> { let project = project.read(cx); match input.mode { EditFileMode::Edit | EditFileMode::Overwrite => { let path = project .find_project_path(&input.path, cx) .context("Can't edit file: path not found")?; let entry = project .entry_for_path(&path, cx) .context("Can't edit file: path not found")?; anyhow::ensure!(entry.is_file(), "Can't edit file: path is a directory"); Ok(path) } EditFileMode::Create => { if let Some(path) = project.find_project_path(&input.path, cx) { anyhow::ensure!( project.entry_for_path(&path, cx).is_none(), "Can't create file: file already exists" ); } let parent_path = input .path .parent() .context("Can't create file: incorrect path")?; let parent_project_path = project.find_project_path(&parent_path, cx); let parent_entry = parent_project_path .as_ref() .and_then(|path| project.entry_for_path(&path, cx)) .context("Can't create file: parent directory doesn't exist")?; anyhow::ensure!( parent_entry.is_dir(), "Can't create file: parent is not a directory" ); let file_name = input .path .file_name() .context("Can't create file: invalid filename")?; let new_file_path = parent_project_path.map(|parent| ProjectPath { path: Arc::from(parent.path.join(file_name)), ..parent }); new_file_path.context("Can't create file") } }} pub struct EditFileToolCard {Lines 1–500 of 1701. Load more
use crate::{ Templates, edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat}, schema::json_schema_for, ui::{COLLAPSED_LINES, ToolOutputPreview}, }; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::{ ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus, }; use buffer_diff::{BufferDiff, BufferDiffSnapshot}; use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, scroll::Autoscroll}; use futures::StreamExt; use gpui::{ Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task, TextStyleRefinement, WeakEntity, pulsating_between, px, }; use indoc::formatdoc; use language::{ Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Point, Rope, TextBuffer, language_settings::{self, FormatOnSave, SoftWrap}, }; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; use markdown::{Markdown, MarkdownElement, MarkdownStyle}; use project::{ Project, ProjectPath, lsp_store::{FormatTrigger, LspFormatTarget}, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; use std::{ cmp::Reverse, collections::HashSet, ops::Range, path::{Path, PathBuf}, sync::Arc, time::Duration, }; use theme::ThemeSettings; use ui::{Disclosure, Tooltip, prelude::*}; use util::ResultExt; use workspace::Workspace; pub struct EditFileTool; #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct EditFileToolInput { /// A one-line, user-friendly markdown description of the edit. This will be /// shown in the UI and also passed to another model to perform the edit. /// /// Be terse, but also descriptive in what you want to achieve with this /// edit. Avoid generic instructions. /// /// NEVER mention the file path in this description. /// /// <example>Fix API endpoint URLs</example> /// <example>Update copyright year in `page_footer`</example> /// /// Make sure to include this field before all the others in the input object /// so that we can display it immediately. pub display_description: String, /// The full path of the file to create or modify in the project. /// /// WARNING: When specifying which file path need changing, you MUST /// start each path with one of the project's root directories. /// /// The following examples assume we have two root directories in the project: /// - /a/b/backend /// - /c/d/frontend /// /// <example> /// `backend/src/main.rs` /// /// Notice how the file path starts with `backend`. Without that, the path /// would be ambiguous and the call would fail! /// </example> /// /// <example> /// `frontend/db.js` /// </example> pub path: PathBuf, /// The mode of operation on the file. Possible values: /// - 'edit': Make granular edits to an existing file. /// - 'create': Create a new file if it doesn't exist. /// - 'overwrite': Replace the entire contents of an existing file. /// /// When a file already exists or you just created it, prefer editing /// it as opposed to recreating it from scratch. pub mode: EditFileMode, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum EditFileMode { Edit, Create, Overwrite, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct EditFileToolOutput { pub original_path: PathBuf, pub new_text: String, pub old_text: Arc<String>, pub raw_output: Option<EditAgentOutput>, } #[derive(Debug, Serialize, Deserialize, JsonSchema)] struct PartialInput { #[serde(default)] path: String, #[serde(default)] display_description: String, } const DEFAULT_UI_TEXT: &str = "Editing file"; impl Tool for EditFileTool { fn name(&self) -> String { "edit_file".into() } fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool { false } fn may_perform_edits(&self) -> bool { true } fn description(&self) -> String { include_str!("edit_file_tool/description.md").to_string() } fn icon(&self) -> IconName { IconName::Pencil } fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> { json_schema_for::<EditFileToolInput>(format) } fn ui_text(&self, input: &serde_json::Value) -> String { match serde_json::from_value::<EditFileToolInput>(input.clone()) { Ok(input) => input.display_description, Err(_) => "Editing file".to_string(), } } fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String { if let Some(input) = serde_json::from_value::<PartialInput>(input.clone()).ok() { let description = input.display_description.trim(); if !description.is_empty() { return description.to_string(); } let path = input.path.trim(); if !path.is_empty() { return path.to_string(); } } DEFAULT_UI_TEXT.to_string() } fn run( self: Arc<Self>, input: serde_json::Value, request: Arc<LanguageModelRequest>, project: Entity<Project>, action_log: Entity<ActionLog>, model: Arc<dyn LanguageModel>, window: Option<AnyWindowHandle>, cx: &mut App, ) -> ToolResult { let input = match serde_json::from_value::<EditFileToolInput>(input) { Ok(input) => input, Err(err) => return Task::ready(Err(anyhow!(err))).into(), }; let project_path = match resolve_path(&input, project.clone(), cx) { Ok(path) => path, Err(err) => return Task::ready(Err(anyhow!(err))).into(), }; let card = window.and_then(|window| { window .update(cx, |_, window, cx| { cx.new(|cx| { EditFileToolCard::new(input.path.clone(), project.clone(), window, cx) }) }) .ok() }); let card_clone = card.clone(); let action_log_clone = action_log.clone(); let task = cx.spawn(async move |cx: &mut AsyncApp| { let edit_format = EditFormat::from_model(model.clone())?; let edit_agent = EditAgent::new( model, project.clone(), action_log_clone, Templates::new(), edit_format, ); let buffer = project .update(cx, |project, cx| { project.open_buffer(project_path.clone(), cx) })? .await?; let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; let old_text = cx .background_spawn({ let old_snapshot = old_snapshot.clone(); async move { Arc::new(old_snapshot.text()) } }) .await; if let Some(card) = card_clone.as_ref() { card.update(cx, |card, cx| card.initialize(buffer.clone(), cx))?; } let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) { edit_agent.edit( buffer.clone(), input.display_description.clone(), &request, cx, ) } else { edit_agent.overwrite( buffer.clone(), input.display_description.clone(), &request, cx, ) }; let mut hallucinated_old_text = false; let mut ambiguous_ranges = Vec::new(); while let Some(event) = events.next().await { match event { EditAgentOutputEvent::Edited => { if let Some(card) = card_clone.as_ref() { card.update(cx, |card, cx| card.update_diff(cx))?; } } EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true, EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges, EditAgentOutputEvent::ResolvingEditRange(range) => { if let Some(card) = card_clone.as_ref() { card.update(cx, |card, cx| card.reveal_range(range, cx))?; } } } } let agent_output = output.await?; // If format_on_save is enabled, format the buffer let format_on_save_enabled = buffer .read_with(cx, |buffer, cx| { let settings = language_settings::language_settings( buffer.language().map(|l| l.name()), buffer.file(), cx, ); !matches!(settings.format_on_save, FormatOnSave::Off) }) .unwrap_or(false); if format_on_save_enabled { let format_task = project.update(cx, |project, cx| { project.format( HashSet::from_iter([buffer.clone()]), LspFormatTarget::Buffers, false, // Don't push to history since the tool did it. FormatTrigger::Save, cx, ) })?; format_task.await.log_err(); } project .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))? .await?; // Notify the action log that we've edited the buffer (*after* formatting has completed). action_log.update(cx, |log, cx| { log.buffer_edited(buffer.clone(), cx); })?; let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; let (new_text, diff) = cx .background_spawn({ let new_snapshot = new_snapshot.clone(); let old_text = old_text.clone(); async move { let new_text = new_snapshot.text(); let diff = language::unified_diff(&old_text, &new_text); (new_text, diff) } }) .await; let output = EditFileToolOutput { original_path: project_path.path.to_path_buf(), new_text: new_text.clone(), old_text, raw_output: Some(agent_output), }; if let Some(card) = card_clone { card.update(cx, |card, cx| { card.update_diff(cx); card.finalize(cx) }) .log_err(); } let input_path = input.path.display(); if diff.is_empty() { anyhow::ensure!( !hallucinated_old_text, formatdoc! {" Some edits were produced but none of them could be applied. Read the relevant sections of {input_path} again so that I can perform the requested edits. "} ); anyhow::ensure!( ambiguous_ranges.is_empty(), { let line_numbers = ambiguous_ranges .iter() .map(|range| range.start.to_string()) .collect::<Vec<_>>() .join(", "); formatdoc! {" <old_text> matches more than one position in the file (lines: {line_numbers}). Read the relevant sections of {input_path} again and extend <old_text> so that I can perform the requested edits. "} } ); Ok(ToolResultOutput { content: ToolResultContent::Text("No edits were made.".into()), output: serde_json::to_value(output).ok(), }) } else { Ok(ToolResultOutput { content: ToolResultContent::Text(format!( "Edited {}:\n\n```diff\n{}\n```", input_path, diff )), output: serde_json::to_value(output).ok(), }) } }); ToolResult { output: task, card: card.map(AnyToolCard::from), } } fn deserialize_card( self: Arc<Self>, output: serde_json::Value, project: Entity<Project>, window: &mut Window, cx: &mut App, ) -> Option<AnyToolCard> { let output = match serde_json::from_value::<EditFileToolOutput>(output) { Ok(output) => output, Err(_) => return None, }; let card = cx.new(|cx| { EditFileToolCard::new(output.original_path.clone(), project.clone(), window, cx) }); cx.spawn({ let path: Arc<Path> = output.original_path.into(); let language_registry = project.read(cx).languages().clone(); let card = card.clone(); async move |cx| { let buffer = build_buffer(output.new_text, path.clone(), &language_registry, cx).await?; let buffer_diff = build_buffer_diff(output.old_text.clone(), &buffer, &language_registry, cx) .await?; card.update(cx, |card, cx| { card.multibuffer.update(cx, |multibuffer, cx| { let snapshot = buffer.read(cx).snapshot(); let diff = buffer_diff.read(cx); let diff_hunk_ranges = diff .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx) .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot)) .collect::<Vec<_>>(); multibuffer.set_excerpts_for_path( PathKey::for_buffer(&buffer, cx), buffer, diff_hunk_ranges, editor::DEFAULT_MULTIBUFFER_CONTEXT, cx, ); multibuffer.add_diff(buffer_diff, cx); let end = multibuffer.len(cx); card.total_lines = Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1); }); cx.notify(); })?; anyhow::Ok(()) } }) .detach_and_log_err(cx); Some(card.into()) } } /// Validate that the file path is valid, meaning: /// /// - For `edit` and `overwrite`, the path must point to an existing file. /// - For `create`, the file must not already exist, but it's parent dir must exist. fn resolve_path( input: &EditFileToolInput, project: Entity<Project>, cx: &mut App, ) -> Result<ProjectPath> { let project = project.read(cx); match input.mode { EditFileMode::Edit | EditFileMode::Overwrite => { let path = project .find_project_path(&input.path, cx) .context("Can't edit file: path not found")?; let entry = project .entry_for_path(&path, cx) .context("Can't edit file: path not found")?; anyhow::ensure!(entry.is_file(), "Can't edit file: path is a directory"); Ok(path) } EditFileMode::Create => { if let Some(path) = project.find_project_path(&input.path, cx) { anyhow::ensure!( project.entry_for_path(&path, cx).is_none(), "Can't create file: file already exists" ); } let parent_path = input .path .parent() .context("Can't create file: incorrect path")?; let parent_project_path = project.find_project_path(&parent_path, cx); let parent_entry = parent_project_path .as_ref() .and_then(|path| project.entry_for_path(&path, cx)) .context("Can't create file: parent directory doesn't exist")?; anyhow::ensure!( parent_entry.is_dir(), "Can't create file: parent is not a directory" ); let file_name = input .path .file_name() .context("Can't create file: invalid filename")?; let new_file_path = parent_project_path.map(|parent| ProjectPath { path: Arc::from(parent.path.join(file_name)), ..parent }); new_file_path.context("Can't create file") } } } pub struct EditFileToolCard {