diff --git a/assets/prompts/step_resolution.hbs b/assets/prompts/step_resolution.hbs index 7331067db6..74bc43e078 100644 --- a/assets/prompts/step_resolution.hbs +++ b/assets/prompts/step_resolution.hbs @@ -1,4 +1,4 @@ -Your task is to map a step from the conversation above to operations on symbols inside the provided source files. +Your task is to map a step from the conversation above to suggestions on symbols inside the provided source files. Guidelines: - There's no need to describe *what* to do, just *where* to do it. @@ -6,13 +6,13 @@ Guidelines: - Don't create and then update a file. - We'll create it in one shot. - Prefer updating symbols lower in the syntax tree if possible. -- Never include operations on a parent symbol and one of its children in the same operations block. -- Never nest an operation with another operation or include CDATA or other content. All operations are leaf nodes. +- Never include suggestions on a parent symbol and one of its children in the same suggestions block. +- Never nest an operation with another operation or include CDATA or other content. All suggestions are leaf nodes. - Include a description attribute for each operation with a brief, one-line description of the change to perform. -- Descriptions are required for all operations except delete. -- When generating multiple operations, ensure the descriptions are specific to each individual operation. +- Descriptions are required for all suggestions except delete. +- When generating multiple suggestions, ensure the descriptions are specific to each individual operation. - Avoid referring to the location in the description. Focus on the change to be made, not the location where it's made. That's implicit with the symbol you provide. -- Don't generate multiple operations at the same location. Instead, combine them together in a single operation with a succinct combined description. +- Don't generate multiple suggestions at the same location. Instead, combine them together in a single operation with a succinct combined description. Example 1: @@ -33,12 +33,12 @@ impl Rectangle { Add new methods 'calculate_area' and 'calculate_perimeter' to the Rectangle struct Implement the 'Display' trait for the Rectangle struct -What are the operations for the step: Add a new method 'calculate_area' to the Rectangle struct +What are the suggestions for the step: Add a new method 'calculate_area' to the Rectangle struct A (wrong): { "title": "Add Rectangle methods", - "operations": [ + "suggestions": [ { "kind": "AppendChild", "path": "src/shapes.rs", @@ -59,7 +59,7 @@ This demonstrates what NOT to do. NEVER append multiple children at the same loc A (corrected): { "title": "Add Rectangle methods", - "operations": [ + "suggestions": [ { "kind": "AppendChild", "path": "src/shapes.rs", @@ -70,12 +70,12 @@ A (corrected): } User: -What are the operations for the step: Implement the 'Display' trait for the Rectangle struct +What are the suggestions for the step: Implement the 'Display' trait for the Rectangle struct A: { "title": "Implement Display for Rectangle", - "operations": [ + "suggestions": [ { "kind": "InsertSiblingAfter", "path": "src/shapes.rs", @@ -109,12 +109,12 @@ impl User { Update the 'print_info' method to use formatted output Remove the 'email' field from the User struct -What are the operations for the step: Update the 'print_info' method to use formatted output +What are the suggestions for the step: Update the 'print_info' method to use formatted output A: { "title": "Use formatted output", - "operations": [ + "suggestions": [ { "kind": "Update", "path": "src/user.rs", @@ -125,12 +125,12 @@ A: } User: -What are the operations for the step: Remove the 'email' field from the User struct +What are the suggestions for the step: Remove the 'email' field from the User struct A: { "title": "Remove email field", - "operations": [ + "suggestions": [ { "kind": "Delete", "path": "src/user.rs", @@ -163,12 +163,12 @@ impl Vehicle { Add a 'use std::fmt;' statement at the beginning of the file Add a new method 'start_engine' in the Vehicle impl block -What are the operations for the step: Add a 'use std::fmt;' statement at the beginning of the file +What are the suggestions for the step: Add a 'use std::fmt;' statement at the beginning of the file A: { "title": "Add use std::fmt statement", - "operations": [ + "suggestions": [ { "kind": "PrependChild", "path": "src/vehicle.rs", @@ -178,12 +178,12 @@ A: } User: -What are the operations for the step: Add a new method 'start_engine' in the Vehicle impl block +What are the suggestions for the step: Add a new method 'start_engine' in the Vehicle impl block A: { "title": "Add start_engine method", - "operations": [ + "suggestions": [ { "kind": "InsertSiblingAfter", "path": "src/vehicle.rs", @@ -222,12 +222,12 @@ impl Employee { Make salary an f32 -What are the operations for the step: Make salary an f32 +What are the suggestions for the step: Make salary an f32 A (wrong): { "title": "Change salary to f32", - "operations": [ + "suggestions": [ { "kind": "Update", "path": "src/employee.rs", @@ -248,7 +248,7 @@ This example demonstrates what not to do. `struct Employee salary` is a child of A (corrected): { "title": "Change salary to f32", - "operations": [ + "suggestions": [ { "kind": "Update", "path": "src/employee.rs", @@ -259,12 +259,12 @@ A (corrected): } User: -What are the correct operations for the step: Remove the 'department' field and update the 'print_details' method +What are the correct suggestions for the step: Remove the 'department' field and update the 'print_details' method A: { "title": "Remove department", - "operations": [ + "suggestions": [ { "kind": "Delete", "path": "src/employee.rs", @@ -311,7 +311,7 @@ impl Game { A: { "title": "Add level field to Player", - "operations": [ + "suggestions": [ { "kind": "InsertSiblingAfter", "path": "src/game.rs", @@ -349,7 +349,7 @@ impl Config { A: { "title": "Add load_from_file method", - "operations": [ + "suggestions": [ { "kind": "PrependChild", "path": "src/config.rs", @@ -389,7 +389,7 @@ impl Database { A: { "title": "Add error handling to query", - "operations": [ + "suggestions": [ { "kind": "PrependChild", "path": "src/database.rs", @@ -410,4 +410,4 @@ A: ] } -Now generate the operations for the following step: +Now generate the suggestions for the following step: diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index d895bd1331..e2fe437b74 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -10,14 +10,14 @@ use crate::{ }, terminal_inline_assistant::TerminalInlineAssistant, Assist, CodegenStatus, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore, - CycleMessageRole, DebugEditSteps, DeployHistory, DeployPromptLibrary, EditSuggestionGroup, - InlineAssist, InlineAssistId, InlineAssistant, InsertIntoEditor, MessageStatus, ModelSelector, + CycleMessageRole, DebugEditSteps, DeployHistory, DeployPromptLibrary, InlineAssist, + InlineAssistId, InlineAssistant, InsertIntoEditor, MessageStatus, ModelSelector, PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata, - ResolvedWorkflowStepEditSuggestions, SavedContextMetadata, Split, ToggleFocus, - ToggleModelSelector, WorkflowStepEditSuggestions, + ResolvedWorkflowStep, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector, + WorkflowStepStatus, }; use crate::{ContextStoreEvent, ShowConfiguration}; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context as _, Result}; use assistant_slash_command::{SlashCommand, SlashCommandOutputSection}; use client::{proto, Client, Status}; use collections::{BTreeSet, HashMap, HashSet}; @@ -41,8 +41,7 @@ use gpui::{ }; use indexed_docs::IndexedDocsStore; use language::{ - language_settings::SoftWrap, Buffer, Capability, LanguageRegistry, LspAdapterDelegate, Point, - ToOffset, + language_settings::SoftWrap, Capability, LanguageRegistry, LspAdapterDelegate, Point, ToOffset, }; use language_model::{ provider::cloud::PROVIDER_ID, LanguageModelProvider, LanguageModelProviderId, @@ -1330,15 +1329,10 @@ struct ScrollPosition { cursor: Anchor, } -struct StepAssists { - assist_ids: Vec, +struct WorkflowAssist { editor: WeakView, -} - -#[derive(Debug, Eq, PartialEq)] -struct ActiveWorkflowStep { - range: Range, - suggestions: Option, + editor_was_open: bool, + assist_ids: Vec, } pub struct ContextEditor { @@ -1353,9 +1347,9 @@ pub struct ContextEditor { remote_id: Option, pending_slash_command_creases: HashMap, CreaseId>, pending_slash_command_blocks: HashMap, CustomBlockId>, + workflow_assists: HashMap, WorkflowAssist>, + active_workflow_step_range: Option>, _subscriptions: Vec, - assists_by_step: HashMap, StepAssists>, - active_workflow_step: Option, assistant_panel: WeakView, error_message: Option, } @@ -1413,8 +1407,8 @@ impl ContextEditor { pending_slash_command_creases: HashMap::default(), pending_slash_command_blocks: HashMap::default(), _subscriptions, - assists_by_step: HashMap::default(), - active_workflow_step: None, + workflow_assists: HashMap::default(), + active_workflow_step_range: None, assistant_panel, error_message: None, }; @@ -1449,16 +1443,16 @@ impl ContextEditor { } fn assist(&mut self, _: &Assist, cx: &mut ViewContext) { - if !self.apply_edit_step(cx) { + if !self.apply_workflow_step(cx) { self.error_message = None; self.send_to_model(cx); cx.notify(); } } - fn apply_edit_step(&mut self, cx: &mut ViewContext) -> bool { - if let Some(step) = self.active_workflow_step.as_ref() { - if let Some(assists) = self.assists_by_step.get(&step.range) { + fn apply_workflow_step(&mut self, cx: &mut ViewContext) -> bool { + if let Some(step_range) = self.active_workflow_step_range.as_ref() { + if let Some(assists) = self.workflow_assists.get(&step_range) { let assist_ids = assists.assist_ids.clone(); cx.window_context().defer(|cx| { InlineAssistant::update_global(cx, |assistant, cx| { @@ -1519,16 +1513,13 @@ impl ContextEditor { .text_for_range(step.tagged_range.clone()) .collect::() )); - match &step.edit_suggestions { - WorkflowStepEditSuggestions::Resolved(ResolvedWorkflowStepEditSuggestions { - title, - edit_suggestions, - }) => { + match &step.status { + WorkflowStepStatus::Resolved(ResolvedWorkflowStep { title, suggestions }) => { output.push_str("Resolution:\n"); output.push_str(&format!(" {:?}\n", title)); - output.push_str(&format!(" {:?}\n", edit_suggestions)); + output.push_str(&format!(" {:?}\n", suggestions)); } - WorkflowStepEditSuggestions::Pending(_) => { + WorkflowStepStatus::Pending(_) => { output.push_str("Resolution: Pending\n"); } } @@ -1676,7 +1667,7 @@ impl ContextEditor { }); } ContextEvent::WorkflowStepsChanged => { - self.update_active_workflow_step(cx); + self.update_active_workflow_step_from_cursor(cx); cx.notify(); } ContextEvent::SummaryChanged => { @@ -1941,14 +1932,14 @@ impl ContextEditor { } EditorEvent::SelectionsChanged { .. } => { self.scroll_position = self.cursor_scroll_position(cx); - self.update_active_workflow_step(cx); + self.update_active_workflow_step_from_cursor(cx); } _ => {} } cx.emit(event.clone()); } - fn update_active_workflow_step(&mut self, cx: &mut ViewContext) { + fn update_active_workflow_step_from_cursor(&mut self, cx: &mut ViewContext) { let new_step = self .workflow_step_range_for_cursor(cx) .as_ref() @@ -1957,14 +1948,11 @@ impl ContextEditor { .context .read(cx) .workflow_step_for_range(step_range.clone())?; - Some(ActiveWorkflowStep { - range: workflow_step.tagged_range.clone(), - suggestions: workflow_step.edit_suggestions.as_resolved().cloned(), - }) + Some(workflow_step.tagged_range.clone()) }); - if new_step.as_ref() != self.active_workflow_step.as_ref() { - if let Some(old_step) = self.active_workflow_step.take() { - self.cancel_workflow_step_if_idle(old_step.range, cx); + if new_step.as_ref() != self.active_workflow_step_range.as_ref() { + if let Some(old_step_range) = self.active_workflow_step_range.take() { + self.hide_workflow_step(old_step_range, cx); } if let Some(new_step) = new_step { @@ -1973,21 +1961,21 @@ impl ContextEditor { } } - fn cancel_workflow_step_if_idle( + fn hide_workflow_step( &mut self, step_range: Range, cx: &mut ViewContext, ) { - let Some(step_assists) = self.assists_by_step.get_mut(&step_range) else { + let Some(step_assist) = self.workflow_assists.get_mut(&step_range) else { return; }; - let Some(editor) = step_assists.editor.upgrade() else { - self.assists_by_step.remove(&step_range); + let Some(editor) = step_assist.editor.upgrade() else { + self.workflow_assists.remove(&step_range); return; }; InlineAssistant::update_global(cx, |assistant, cx| { - step_assists.assist_ids.retain(|assist_id| { + step_assist.assist_ids.retain(|assist_id| { match assistant.status_for_assist(*assist_id, cx) { Some(CodegenStatus::Idle) | None => { assistant.finish_assist(*assist_id, true, cx); @@ -1998,14 +1986,15 @@ impl ContextEditor { }); }); - if step_assists.assist_ids.is_empty() { - self.assists_by_step.remove(&step_range); + if step_assist.assist_ids.is_empty() { + let editor_was_open = step_assist.editor_was_open; + self.workflow_assists.remove(&step_range); self.workspace .update(cx, |workspace, cx| { if let Some(pane) = workspace.pane_for(&editor) { pane.update(cx, |pane, cx| { let item_id = editor.entity_id(); - if pane.is_active_preview_item(item_id) { + if !editor_was_open && pane.is_active_preview_item(item_id) { pane.close_item_by_id(item_id, SaveIntent::Skip, cx) .detach_and_log_err(cx); } @@ -2016,147 +2005,200 @@ impl ContextEditor { } } - fn activate_workflow_step(&mut self, step: ActiveWorkflowStep, cx: &mut ViewContext) { - if let Some(step_assists) = self.assists_by_step.get(&step.range) { - if let Some(editor) = step_assists.editor.upgrade() { - for assist_id in &step_assists.assist_ids { - match InlineAssistant::global(cx).status_for_assist(*assist_id, cx) { - Some(CodegenStatus::Idle) | None => {} - _ => { - self.workspace - .update(cx, |workspace, cx| { - workspace.activate_item(&editor, false, false, cx); - }) - .ok(); - InlineAssistant::update_global(cx, |assistant, cx| { - assistant.scroll_to_assist(*assist_id, cx) - }); - return; - } - } - } - } - } - - if let Some(ResolvedWorkflowStepEditSuggestions { - title, - edit_suggestions, - }) = step.suggestions.as_ref() - { - if let Some((editor, assist_ids)) = - self.suggest_edits(title.clone(), edit_suggestions.clone(), cx) - { - self.assists_by_step.insert( - step.range.clone(), - StepAssists { - assist_ids, - editor: editor.downgrade(), - }, - ); - } - } - - self.active_workflow_step = Some(step); - } - - fn suggest_edits( + fn activate_workflow_step( &mut self, - title: String, - edit_suggestions: HashMap, Vec>, + step_range: Range, cx: &mut ViewContext, - ) -> Option<(View, Vec)> { - let assistant_panel = self.assistant_panel.upgrade()?; - if edit_suggestions.is_empty() { + ) -> Option<()> { + if self.scroll_to_existing_workflow_assist(&step_range, cx) { return None; } - let editor; - let mut suggestion_groups = Vec::new(); - if edit_suggestions.len() == 1 && edit_suggestions.values().next().unwrap().len() == 1 { - // If there's only one buffer and one suggestion group, open it directly - let (buffer, groups) = edit_suggestions.into_iter().next().unwrap(); - let group = groups.into_iter().next().unwrap(); - editor = self - .workspace - .update(cx, |workspace, cx| { - let active_pane = workspace.active_pane().clone(); - workspace.open_project_item::(active_pane, buffer, false, false, cx) - }) - .log_err()?; + let step = self + .workflow_step(&step_range, cx) + .with_context(|| format!("could not find workflow step for range {:?}", step_range)) + .log_err()?; + let Some(resolved) = step.status.as_resolved() else { + return None; + }; - let (&excerpt_id, _, _) = editor - .read(cx) - .buffer() - .read(cx) - .read(cx) - .as_singleton() - .unwrap(); + let title = resolved.title.clone(); + let suggestions = resolved.suggestions.clone(); - // Scroll the editor to the suggested assist - editor.update(cx, |editor, cx| { - let multibuffer = editor.buffer().read(cx).snapshot(cx); - let (&excerpt_id, _, buffer) = multibuffer.as_singleton().unwrap(); - let anchor = if group.context_range.start.to_offset(buffer) == 0 { - Anchor::min() - } else { - multibuffer - .anchor_in_excerpt(excerpt_id, group.context_range.start) - .unwrap() - }; + if let Some((editor, assist_ids, editor_was_open)) = { + let assistant_panel = self.assistant_panel.upgrade()?; + if suggestions.is_empty() { + return None; + } - editor.set_scroll_anchor( - ScrollAnchor { - offset: gpui::Point::default(), - anchor, - }, - cx, - ); - }); + let editor; + let mut editor_was_open = false; + let mut suggestion_groups = Vec::new(); + if suggestions.len() == 1 && suggestions.values().next().unwrap().len() == 1 { + // If there's only one buffer and one suggestion group, open it directly + let (buffer, groups) = suggestions.into_iter().next().unwrap(); + let group = groups.into_iter().next().unwrap(); + editor = self + .workspace + .update(cx, |workspace, cx| { + let active_pane = workspace.active_pane().clone(); + editor_was_open = + workspace.is_project_item_open::(&active_pane, &buffer, cx); + workspace.open_project_item::(active_pane, buffer, false, false, cx) + }) + .log_err()?; - suggestion_groups.push((excerpt_id, group)); - } else { - // If there are multiple buffers or suggestion groups, create a multibuffer - let multibuffer = cx.new_model(|cx| { - let replica_id = self.project.read(cx).replica_id(); - let mut multibuffer = - MultiBuffer::new(replica_id, Capability::ReadWrite).with_title(title); - for (buffer, groups) in edit_suggestions { - let excerpt_ids = multibuffer.push_excerpts( - buffer, - groups.iter().map(|suggestion_group| ExcerptRange { - context: suggestion_group.context_range.clone(), - primary: None, - }), + let (&excerpt_id, _, _) = editor + .read(cx) + .buffer() + .read(cx) + .read(cx) + .as_singleton() + .unwrap(); + + // Scroll the editor to the suggested assist + editor.update(cx, |editor, cx| { + let multibuffer = editor.buffer().read(cx).snapshot(cx); + let (&excerpt_id, _, buffer) = multibuffer.as_singleton().unwrap(); + let anchor = if group.context_range.start.to_offset(buffer) == 0 { + Anchor::min() + } else { + multibuffer + .anchor_in_excerpt(excerpt_id, group.context_range.start) + .unwrap() + }; + + editor.set_scroll_anchor( + ScrollAnchor { + offset: gpui::Point::default(), + anchor, + }, cx, ); - suggestion_groups.extend(excerpt_ids.into_iter().zip(groups)); - } - multibuffer - }); + }); - editor = cx.new_view(|cx| { - Editor::for_multibuffer(multibuffer, Some(self.project.clone()), true, cx) - }); - self.workspace - .update(cx, |workspace, cx| { - workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx) - }) - .log_err()?; + suggestion_groups.push((excerpt_id, group)); + } else { + // If there are multiple buffers or suggestion groups, create a multibuffer + let multibuffer = cx.new_model(|cx| { + let replica_id = self.project.read(cx).replica_id(); + let mut multibuffer = + MultiBuffer::new(replica_id, Capability::ReadWrite).with_title(title); + for (buffer, groups) in suggestions { + let excerpt_ids = multibuffer.push_excerpts( + buffer, + groups.iter().map(|suggestion_group| ExcerptRange { + context: suggestion_group.context_range.clone(), + primary: None, + }), + cx, + ); + suggestion_groups.extend(excerpt_ids.into_iter().zip(groups)); + } + multibuffer + }); + + editor = cx.new_view(|cx| { + Editor::for_multibuffer(multibuffer, Some(self.project.clone()), true, cx) + }); + self.workspace + .update(cx, |workspace, cx| { + workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx) + }) + .log_err()?; + } + + let mut assist_ids = Vec::new(); + for (excerpt_id, suggestion_group) in suggestion_groups { + for suggestion in suggestion_group.suggestions { + assist_ids.extend(suggestion.show( + &editor, + excerpt_id, + &self.workspace, + &assistant_panel, + cx, + )); + } + } + + if let Some(range) = self.active_workflow_step_range.clone() { + self.workflow_assists.insert( + range, + WorkflowAssist { + assist_ids: assist_ids.clone(), + editor: editor.downgrade(), + editor_was_open, + }, + ); + } + + Some((editor, assist_ids, editor_was_open)) + } { + self.workflow_assists.insert( + step_range.clone(), + WorkflowAssist { + assist_ids, + editor_was_open, + editor: editor.downgrade(), + }, + ); } - let mut assist_ids = Vec::new(); - for (excerpt_id, suggestion_group) in suggestion_groups { - for suggestion in suggestion_group.suggestions { - assist_ids.extend(suggestion.show( - &editor, - excerpt_id, - &self.workspace, - &assistant_panel, - cx, - )); + self.active_workflow_step_range = Some(step_range); + + Some(()) + } + + fn active_workflow_step<'a>(&'a self, cx: &'a AppContext) -> Option<&'a crate::WorkflowStep> { + self.active_workflow_step_range + .as_ref() + .and_then(|step_range| { + self.context + .read(cx) + .workflow_step_for_range(step_range.clone()) + }) + } + + fn workflow_step<'a>( + &'a mut self, + step_range: &Range, + cx: &'a mut ViewContext, + ) -> Option<&'a crate::WorkflowStep> { + self.context + .read(cx) + .workflow_step_for_range(step_range.clone()) + } + + fn scroll_to_existing_workflow_assist( + &self, + step_range: &Range, + cx: &mut ViewContext, + ) -> bool { + let step_assists = match self.workflow_assists.get(step_range) { + Some(assists) => assists, + None => return false, + }; + let editor = match step_assists.editor.upgrade() { + Some(editor) => editor, + None => return false, + }; + for assist_id in &step_assists.assist_ids { + match InlineAssistant::global(cx).status_for_assist(*assist_id, cx) { + Some(CodegenStatus::Idle) | None => {} + _ => { + self.workspace + .update(cx, |workspace, cx| { + workspace.activate_item(&editor, false, false, cx); + }) + .ok(); + InlineAssistant::update_global(cx, |assistant, cx| { + assistant.scroll_to_assist(*assist_id, cx) + }); + return true; + } } } - Some((editor, assist_ids)) + false } fn handle_editor_search_event( @@ -2540,12 +2582,12 @@ impl ContextEditor { fn render_send_button(&self, cx: &mut ViewContext) -> impl IntoElement { let focus_handle = self.focus_handle(cx).clone(); - let button_text = match self.active_workflow_step.as_ref() { + let button_text = match self.active_workflow_step(cx) { Some(step) => { - if step.suggestions.is_none() { - "Computing Changes..." - } else { + if step.status.is_resolved() { "Apply Changes" + } else { + "Computing Changes..." } } None => "Send", diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index b001794d9e..33a8961800 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -348,37 +348,44 @@ pub struct SlashCommandId(clock::Lamport); #[derive(Debug)] pub struct WorkflowStep { pub tagged_range: Range, - pub edit_suggestions: WorkflowStepEditSuggestions, + pub status: WorkflowStepStatus, } #[derive(Clone, Debug, Eq, PartialEq)] -pub struct ResolvedWorkflowStepEditSuggestions { +pub struct ResolvedWorkflowStep { pub title: String, - pub edit_suggestions: HashMap, Vec>, + pub suggestions: HashMap, Vec>, } -pub enum WorkflowStepEditSuggestions { +pub enum WorkflowStepStatus { Pending(Task>), - Resolved(ResolvedWorkflowStepEditSuggestions), + Resolved(ResolvedWorkflowStep), } -impl WorkflowStepEditSuggestions { - pub fn as_resolved(&self) -> Option<&ResolvedWorkflowStepEditSuggestions> { +impl WorkflowStepStatus { + pub fn as_resolved(&self) -> Option<&ResolvedWorkflowStep> { match self { - WorkflowStepEditSuggestions::Resolved(suggestions) => Some(suggestions), - WorkflowStepEditSuggestions::Pending(_) => None, + WorkflowStepStatus::Resolved(suggestions) => Some(suggestions), + WorkflowStepStatus::Pending(_) => None, + } + } + + pub fn is_resolved(&self) -> bool { + match self { + WorkflowStepStatus::Resolved(_) => true, + WorkflowStepStatus::Pending(_) => false, } } } #[derive(Clone, Debug, Eq, PartialEq)] -pub struct EditSuggestionGroup { +pub struct WorkflowSuggestionGroup { pub context_range: Range, - pub suggestions: Vec, + pub suggestions: Vec, } #[derive(Clone, Debug, Eq, PartialEq)] -pub enum EditSuggestion { +pub enum WorkflowSuggestion { Update { range: Range, description: String, @@ -407,40 +414,40 @@ pub enum EditSuggestion { }, } -impl EditSuggestion { +impl WorkflowSuggestion { pub fn range(&self) -> Range { match self { - EditSuggestion::Update { range, .. } => range.clone(), - EditSuggestion::CreateFile { .. } => language::Anchor::MIN..language::Anchor::MAX, - EditSuggestion::InsertSiblingBefore { position, .. } - | EditSuggestion::InsertSiblingAfter { position, .. } - | EditSuggestion::PrependChild { position, .. } - | EditSuggestion::AppendChild { position, .. } => *position..*position, - EditSuggestion::Delete { range } => range.clone(), + WorkflowSuggestion::Update { range, .. } => range.clone(), + WorkflowSuggestion::CreateFile { .. } => language::Anchor::MIN..language::Anchor::MAX, + WorkflowSuggestion::InsertSiblingBefore { position, .. } + | WorkflowSuggestion::InsertSiblingAfter { position, .. } + | WorkflowSuggestion::PrependChild { position, .. } + | WorkflowSuggestion::AppendChild { position, .. } => *position..*position, + WorkflowSuggestion::Delete { range } => range.clone(), } } pub fn description(&self) -> Option<&str> { match self { - EditSuggestion::Update { description, .. } - | EditSuggestion::CreateFile { description } - | EditSuggestion::InsertSiblingBefore { description, .. } - | EditSuggestion::InsertSiblingAfter { description, .. } - | EditSuggestion::PrependChild { description, .. } - | EditSuggestion::AppendChild { description, .. } => Some(description), - EditSuggestion::Delete { .. } => None, + WorkflowSuggestion::Update { description, .. } + | WorkflowSuggestion::CreateFile { description } + | WorkflowSuggestion::InsertSiblingBefore { description, .. } + | WorkflowSuggestion::InsertSiblingAfter { description, .. } + | WorkflowSuggestion::PrependChild { description, .. } + | WorkflowSuggestion::AppendChild { description, .. } => Some(description), + WorkflowSuggestion::Delete { .. } => None, } } fn description_mut(&mut self) -> Option<&mut String> { match self { - EditSuggestion::Update { description, .. } - | EditSuggestion::CreateFile { description } - | EditSuggestion::InsertSiblingBefore { description, .. } - | EditSuggestion::InsertSiblingAfter { description, .. } - | EditSuggestion::PrependChild { description, .. } - | EditSuggestion::AppendChild { description, .. } => Some(description), - EditSuggestion::Delete { .. } => None, + WorkflowSuggestion::Update { description, .. } + | WorkflowSuggestion::CreateFile { description } + | WorkflowSuggestion::InsertSiblingBefore { description, .. } + | WorkflowSuggestion::InsertSiblingAfter { description, .. } + | WorkflowSuggestion::PrependChild { description, .. } + | WorkflowSuggestion::AppendChild { description, .. } => Some(description), + WorkflowSuggestion::Delete { .. } => None, } } @@ -479,16 +486,16 @@ impl EditSuggestion { let snapshot = buffer.read(cx).snapshot(cx); match self { - EditSuggestion::Update { range, description } => { + WorkflowSuggestion::Update { range, description } => { initial_prompt = description.clone(); suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)? ..snapshot.anchor_in_excerpt(excerpt_id, range.end)?; } - EditSuggestion::CreateFile { description } => { + WorkflowSuggestion::CreateFile { description } => { initial_prompt = description.clone(); suggestion_range = editor::Anchor::min()..editor::Anchor::min(); } - EditSuggestion::InsertSiblingBefore { + WorkflowSuggestion::InsertSiblingBefore { position, description, } => { @@ -498,12 +505,13 @@ impl EditSuggestion { buffer.start_transaction(cx); let line_start = buffer.insert_empty_line(position, true, true, cx); initial_transaction_id = buffer.end_transaction(cx); + buffer.refresh_preview(cx); let line_start = buffer.read(cx).anchor_before(line_start); line_start..line_start }); } - EditSuggestion::InsertSiblingAfter { + WorkflowSuggestion::InsertSiblingAfter { position, description, } => { @@ -513,12 +521,13 @@ impl EditSuggestion { buffer.start_transaction(cx); let line_start = buffer.insert_empty_line(position, true, true, cx); initial_transaction_id = buffer.end_transaction(cx); + buffer.refresh_preview(cx); let line_start = buffer.read(cx).anchor_before(line_start); line_start..line_start }); } - EditSuggestion::PrependChild { + WorkflowSuggestion::PrependChild { position, description, } => { @@ -528,12 +537,13 @@ impl EditSuggestion { buffer.start_transaction(cx); let line_start = buffer.insert_empty_line(position, false, true, cx); initial_transaction_id = buffer.end_transaction(cx); + buffer.refresh_preview(cx); let line_start = buffer.read(cx).anchor_before(line_start); line_start..line_start }); } - EditSuggestion::AppendChild { + WorkflowSuggestion::AppendChild { position, description, } => { @@ -543,12 +553,13 @@ impl EditSuggestion { buffer.start_transaction(cx); let line_start = buffer.insert_empty_line(position, true, false, cx); initial_transaction_id = buffer.end_transaction(cx); + buffer.refresh_preview(cx); let line_start = buffer.read(cx).anchor_before(line_start); line_start..line_start }); } - EditSuggestion::Delete { range } => { + WorkflowSuggestion::Delete { range } => { initial_prompt = "Delete".to_string(); suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)? ..snapshot.anchor_in_excerpt(excerpt_id, range.end)?; @@ -569,17 +580,14 @@ impl EditSuggestion { } } -impl Debug for WorkflowStepEditSuggestions { +impl Debug for WorkflowStepStatus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - WorkflowStepEditSuggestions::Pending(_) => write!(f, "EditStepOperations::Pending"), - WorkflowStepEditSuggestions::Resolved(ResolvedWorkflowStepEditSuggestions { - title, - edit_suggestions, - }) => f + WorkflowStepStatus::Pending(_) => write!(f, "EditStepOperations::Pending"), + WorkflowStepStatus::Resolved(ResolvedWorkflowStep { title, suggestions }) => f .debug_struct("EditStepOperations::Parsed") .field("title", title) - .field("edit_suggestions", edit_suggestions) + .field("suggestions", suggestions) .finish(), } } @@ -1205,16 +1213,13 @@ impl Context { if let Err(ix) = existing_step_index { // Step doesn't exist, so add it - let task = self.compute_workflow_step_edit_suggestions( - tagged_range.clone(), - project.clone(), - cx, - ); + let task = + self.resolve_workflow_step(tagged_range.clone(), project.clone(), cx); new_edit_steps.push(( ix, WorkflowStep { tagged_range, - edit_suggestions: WorkflowStepEditSuggestions::Pending(task), + status: WorkflowStepStatus::Pending(task), }, )); } @@ -1235,7 +1240,7 @@ impl Context { cx.notify(); } - fn compute_workflow_step_edit_suggestions( + fn resolve_workflow_step( &self, tagged_range: Range, project: Model, @@ -1265,13 +1270,13 @@ impl Context { }); // Invoke the model to get its edit suggestions for this workflow step. - let step_suggestions = model - .use_tool::(request, &cx) + let resolution = model + .use_tool::(request, &cx) .await?; // Translate the parsed suggestions to our internal types, which anchor the suggestions to locations in the code. - let suggestion_tasks: Vec<_> = step_suggestions - .edit_suggestions + let suggestion_tasks: Vec<_> = resolution + .suggestions .iter() .map(|suggestion| suggestion.resolve(project.clone(), cx.clone())) .collect(); @@ -1293,7 +1298,7 @@ impl Context { let mut suggestion_groups_by_buffer = HashMap::default(); for (buffer, mut suggestions) in suggestions_by_buffer { - let mut suggestion_groups = Vec::::new(); + let mut suggestion_groups = Vec::::new(); let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?; // Sort suggestions by their range so that earlier, larger ranges come first suggestions.sort_by(|a, b| a.range().cmp(&b.range(), &snapshot)); @@ -1328,14 +1333,14 @@ impl Context { last_group.suggestions.push(suggestion); } else { // Create a new group - suggestion_groups.push(EditSuggestionGroup { + suggestion_groups.push(WorkflowSuggestionGroup { context_range, suggestions: vec![suggestion], }); } } else { // Create the first group - suggestion_groups.push(EditSuggestionGroup { + suggestion_groups.push(WorkflowSuggestionGroup { context_range, suggestions: vec![suggestion], }); @@ -1353,12 +1358,10 @@ impl Context { }) .map_err(|_| anyhow!("edit step not found"))?; if let Some(edit_step) = this.workflow_steps.get_mut(step_index) { - edit_step.edit_suggestions = WorkflowStepEditSuggestions::Resolved( - ResolvedWorkflowStepEditSuggestions { - title: step_suggestions.step_title, - edit_suggestions: suggestion_groups_by_buffer, - }, - ); + edit_step.status = WorkflowStepStatus::Resolved(ResolvedWorkflowStep { + title: resolution.step_title, + suggestions: suggestion_groups_by_buffer, + }); cx.emit(ContextEvent::WorkflowStepsChanged); } anyhow::Ok(()) @@ -3022,19 +3025,17 @@ mod tests { model .as_fake() - .respond_to_last_tool_use(Ok(serde_json::to_value( - tool::WorkflowStepEditSuggestions { - step_title: "Title".into(), - edit_suggestions: vec![tool::EditSuggestion { - path: "/root/hello.rs".into(), - // Simulate a symbol name that's slightly different than our outline query - kind: tool::EditSuggestionKind::Update { - symbol: "fn main()".into(), - description: "Extract a greeting function".into(), - }, - }], - }, - ) + .respond_to_last_tool_use(Ok(serde_json::to_value(tool::WorkflowStepResolution { + step_title: "Title".into(), + suggestions: vec![tool::WorkflowSuggestion { + path: "/root/hello.rs".into(), + // Simulate a symbol name that's slightly different than our outline query + kind: tool::WorkflowSuggestionKind::Update { + symbol: "fn main()".into(), + description: "Extract a greeting function".into(), + }, + }], + }) .unwrap())); // Wait for tool use to be processed. @@ -3074,11 +3075,9 @@ mod tests { .iter() .map(|step| { let buffer = context.buffer.read(cx); - let status = match &step.edit_suggestions { - WorkflowStepEditSuggestions::Pending(_) => { - WorkflowStepEditSuggestionStatus::Pending - } - WorkflowStepEditSuggestions::Resolved { .. } => { + let status = match &step.status { + WorkflowStepStatus::Pending(_) => WorkflowStepEditSuggestionStatus::Pending, + WorkflowStepStatus::Resolved { .. } => { WorkflowStepEditSuggestionStatus::Resolved } }; @@ -3490,15 +3489,15 @@ mod tool { use super::*; #[derive(Debug, Serialize, Deserialize, JsonSchema)] - pub struct WorkflowStepEditSuggestions { + pub struct WorkflowStepResolution { /// An extremely short title for the edit step represented by these operations. pub step_title: String, /// A sequence of operations to apply to the codebase. /// When multiple operations are required for a step, be sure to include multiple operations in this list. - pub edit_suggestions: Vec, + pub suggestions: Vec, } - impl LanguageModelTool for WorkflowStepEditSuggestions { + impl LanguageModelTool for WorkflowStepResolution { fn name() -> String { "edit".into() } @@ -3527,19 +3526,19 @@ mod tool { /// programmatic changes to source code. It provides a structured way to describe /// edits for features like refactoring tools or AI-assisted coding suggestions. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] - pub struct EditSuggestion { + pub struct WorkflowSuggestion { /// The path to the file containing the relevant operation pub path: String, #[serde(flatten)] - pub kind: EditSuggestionKind, + pub kind: WorkflowSuggestionKind, } - impl EditSuggestion { + impl WorkflowSuggestion { pub(super) async fn resolve( &self, project: Model, mut cx: AsyncAppContext, - ) -> Result<(Model, super::EditSuggestion)> { + ) -> Result<(Model, super::WorkflowSuggestion)> { let path = self.path.clone(); let kind = self.kind.clone(); let buffer = project @@ -3561,7 +3560,7 @@ mod tool { let suggestion; match kind { - EditSuggestionKind::Update { + WorkflowSuggestionKind::Update { symbol, description, } => { @@ -3578,12 +3577,12 @@ mod tool { snapshot.line_len(symbol.range.end.row), ); let range = snapshot.anchor_before(start)..snapshot.anchor_after(end); - suggestion = super::EditSuggestion::Update { range, description }; + suggestion = super::WorkflowSuggestion::Update { range, description }; } - EditSuggestionKind::Create { description } => { - suggestion = super::EditSuggestion::CreateFile { description }; + WorkflowSuggestionKind::Create { description } => { + suggestion = super::WorkflowSuggestion::CreateFile { description }; } - EditSuggestionKind::InsertSiblingBefore { + WorkflowSuggestionKind::InsertSiblingBefore { symbol, description, } => { @@ -3598,12 +3597,12 @@ mod tool { annotation_range.start }), ); - suggestion = super::EditSuggestion::InsertSiblingBefore { + suggestion = super::WorkflowSuggestion::InsertSiblingBefore { position, description, }; } - EditSuggestionKind::InsertSiblingAfter { + WorkflowSuggestionKind::InsertSiblingAfter { symbol, description, } => { @@ -3612,12 +3611,12 @@ mod tool { .with_context(|| format!("symbol not found: {:?}", symbol))? .to_point(&snapshot); let position = snapshot.anchor_after(symbol.range.end); - suggestion = super::EditSuggestion::InsertSiblingAfter { + suggestion = super::WorkflowSuggestion::InsertSiblingAfter { position, description, }; } - EditSuggestionKind::PrependChild { + WorkflowSuggestionKind::PrependChild { symbol, description, } => { @@ -3632,18 +3631,18 @@ mod tool { .body_range .map_or(symbol.range.start, |body_range| body_range.start), ); - suggestion = super::EditSuggestion::PrependChild { + suggestion = super::WorkflowSuggestion::PrependChild { position, description, }; } else { - suggestion = super::EditSuggestion::PrependChild { + suggestion = super::WorkflowSuggestion::PrependChild { position: language::Anchor::MIN, description, }; } } - EditSuggestionKind::AppendChild { + WorkflowSuggestionKind::AppendChild { symbol, description, } => { @@ -3658,18 +3657,18 @@ mod tool { .body_range .map_or(symbol.range.end, |body_range| body_range.end), ); - suggestion = super::EditSuggestion::AppendChild { + suggestion = super::WorkflowSuggestion::AppendChild { position, description, }; } else { - suggestion = super::EditSuggestion::PrependChild { + suggestion = super::WorkflowSuggestion::PrependChild { position: language::Anchor::MAX, description, }; } } - EditSuggestionKind::Delete { symbol } => { + WorkflowSuggestionKind::Delete { symbol } => { let symbol = outline .find_most_similar(&symbol) .with_context(|| format!("symbol not found: {:?}", symbol))? @@ -3683,7 +3682,7 @@ mod tool { snapshot.line_len(symbol.range.end.row), ); let range = snapshot.anchor_before(start)..snapshot.anchor_after(end); - suggestion = super::EditSuggestion::Delete { range }; + suggestion = super::WorkflowSuggestion::Delete { range }; } } @@ -3693,7 +3692,7 @@ mod tool { #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(tag = "kind")] - pub enum EditSuggestionKind { + pub enum WorkflowSuggestionKind { /// Rewrites the specified symbol entirely based on the given description. /// This operation completely replaces the existing symbol with new content. Update { @@ -3754,7 +3753,7 @@ mod tool { }, } - impl EditSuggestionKind { + impl WorkflowSuggestionKind { pub fn symbol(&self) -> Option<&str> { match self { Self::Update { symbol, .. } => Some(symbol), @@ -3781,14 +3780,14 @@ mod tool { pub fn initial_insertion(&self) -> Option { match self { - EditSuggestionKind::InsertSiblingBefore { .. } => { + WorkflowSuggestionKind::InsertSiblingBefore { .. } => { Some(InitialInsertion::NewlineAfter) } - EditSuggestionKind::InsertSiblingAfter { .. } => { + WorkflowSuggestionKind::InsertSiblingAfter { .. } => { Some(InitialInsertion::NewlineBefore) } - EditSuggestionKind::PrependChild { .. } => Some(InitialInsertion::NewlineAfter), - EditSuggestionKind::AppendChild { .. } => Some(InitialInsertion::NewlineBefore), + WorkflowSuggestionKind::PrependChild { .. } => Some(InitialInsertion::NewlineAfter), + WorkflowSuggestionKind::AppendChild { .. } => Some(InitialInsertion::NewlineBefore), _ => None, } } diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 123ea30e87..27d05f7deb 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -2156,7 +2156,7 @@ impl Codegen { if let Some(transformation_transaction_id) = self.transformation_transaction_id.take() { self.buffer.update(cx, |buffer, cx| { - buffer.undo_transaction(transformation_transaction_id, cx) + buffer.undo_transaction(transformation_transaction_id, cx); }); } @@ -2510,10 +2510,12 @@ impl Codegen { self.buffer.update(cx, |buffer, cx| { if let Some(transaction_id) = self.transformation_transaction_id.take() { buffer.undo_transaction(transaction_id, cx); + buffer.refresh_preview(cx); } if let Some(transaction_id) = self.initial_transaction_id.take() { buffer.undo_transaction(transaction_id, cx); + buffer.refresh_preview(cx); } }); } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 50b91f4465..696e4b1fe3 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -894,6 +894,10 @@ impl Item for Editor { _ => {} } } + + fn preserve_preview(&self, cx: &AppContext) -> bool { + self.buffer.read(cx).preserve_preview(cx) + } } impl SerializableItem for Editor { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 2eda498127..3ee2d6ca55 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -97,6 +97,7 @@ pub struct Buffer { /// The version vector when this buffer was last loaded from /// or saved to disk. saved_version: clock::Global, + preview_version: clock::Global, transaction_depth: usize, was_dirty_before_starting_transaction: Option, reload_task: Option>>, @@ -703,6 +704,7 @@ impl Buffer { Self { saved_mtime, saved_version: buffer.version(), + preview_version: buffer.version(), reload_task: None, transaction_depth: 0, was_dirty_before_starting_transaction: None, @@ -1351,7 +1353,11 @@ impl Buffer { }) .collect(); + let preserve_preview = self.preserve_preview(); self.edit(edits, None, cx); + if preserve_preview { + self.refresh_preview(); + } } /// Create a minimal edit that will cause the given row to be indented @@ -2195,6 +2201,18 @@ impl Buffer { pub fn completion_triggers(&self) -> &[String] { &self.completion_triggers } + + /// Call this directly after performing edits to prevent the preview tab + /// from being dismissed by those edits. It causes `should_dismiss_preview` + /// to return false until there are additional edits. + pub fn refresh_preview(&mut self) { + self.preview_version = self.version.clone(); + } + + /// Whether we should preserve the preview status of a tab containing this buffer. + pub fn preserve_preview(&self) -> bool { + !self.has_edits_since(&self.preview_version) + } } #[doc(hidden)] diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 2e342e52f3..5924a876d0 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -1822,6 +1822,63 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) { }); } +#[gpui::test] +async fn test_async_autoindents_preserve_preview(cx: &mut TestAppContext) { + cx.update(|cx| init_settings(cx, |_| {})); + + // First we insert some newlines to request an auto-indent (asynchronously). + // Then we request that a preview tab be preserved for the new version, even though it's edited. + let buffer = cx.new_model(|cx| { + let text = "fn a() {}"; + let mut buffer = Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx); + + // This causes autoindent to be async. + buffer.set_sync_parse_timeout(Duration::ZERO); + + buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx); + buffer.refresh_preview(); + + // Synchronously, we haven't auto-indented and we're still preserving the preview. + assert_eq!(buffer.text(), "fn a() {\n\n}"); + assert!(buffer.preserve_preview()); + buffer + }); + + // Now let the autoindent finish + cx.executor().run_until_parked(); + + // The auto-indent applied, but didn't dismiss our preview + buffer.update(cx, |buffer, cx| { + assert_eq!(buffer.text(), "fn a() {\n \n}"); + assert!(buffer.preserve_preview()); + + // Edit inserting another line. It will autoindent async. + // Then refresh the preview version. + buffer.edit( + [(Point::new(1, 4)..Point::new(1, 4), "\n")], + Some(AutoindentMode::EachLine), + cx, + ); + buffer.refresh_preview(); + assert_eq!(buffer.text(), "fn a() {\n \n\n}"); + assert!(buffer.preserve_preview()); + + // Then perform another edit, this time without refreshing the preview version. + buffer.edit([(Point::new(1, 4)..Point::new(1, 4), "x")], None, cx); + // This causes the preview to not be preserved. + assert!(!buffer.preserve_preview()); + }); + + // Let the async autoindent from the first edit finish. + cx.executor().run_until_parked(); + + // The autoindent applies, but it shouldn't restore the preview status because we had an edit in the meantime. + buffer.update(cx, |buffer, _| { + assert_eq!(buffer.text(), "fn a() {\n x\n \n}"); + assert!(!buffer.preserve_preview()); + }); +} + #[gpui::test] fn test_insert_empty_line(cx: &mut AppContext) { init_settings(cx, |_| {}); diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index d2334517fd..abf73cf78d 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -1762,6 +1762,23 @@ impl MultiBuffer { cx.notify(); } + /// Preserve preview tabs containing this multibuffer until additional edits occur. + pub fn refresh_preview(&self, cx: &mut ModelContext) { + for buffer_state in self.buffers.borrow().values() { + buffer_state + .buffer + .update(cx, |buffer, _cx| buffer.refresh_preview()); + } + } + + /// Whether we should preserve the preview status of a tab containing this multi-buffer. + pub fn preserve_preview(&self, cx: &AppContext) -> bool { + self.buffers + .borrow() + .values() + .all(|state| state.buffer.read(cx).preserve_preview()) + } + #[cfg(any(test, feature = "test-support"))] pub fn is_parsing(&self, cx: &AppContext) -> bool { self.as_singleton().unwrap().read(cx).is_parsing() diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index cfa3b2fe2d..7e66e48358 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -287,6 +287,10 @@ pub trait Item: FocusableView + EventEmitter { fn pixel_position_of_cursor(&self, _: &AppContext) -> Option> { None } + + fn preserve_preview(&self, _cx: &AppContext) -> bool { + false + } } pub trait SerializableItem: Item { @@ -427,6 +431,7 @@ pub trait ItemHandle: 'static + Send { fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option>; fn downgrade_item(&self) -> Box; fn workspace_settings<'a>(&self, cx: &'a AppContext) -> &'a WorkspaceSettings; + fn preserve_preview(&self, cx: &AppContext) -> bool; } pub trait WeakItemHandle: Send + Sync { @@ -818,6 +823,10 @@ impl ItemHandle for View { ) -> Option> { SerializableItemRegistry::view_to_serializable_item_handle(self.to_any(), cx) } + + fn preserve_preview(&self, cx: &AppContext) -> bool { + self.read(cx).preserve_preview(cx) + } } impl From> for AnyView { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 414d62febb..8ee5ef7a3e 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -665,6 +665,12 @@ impl Pane { self.preview_item_id } + pub fn preview_item(&self) -> Option> { + self.preview_item_id + .and_then(|id| self.items.iter().find(|item| item.item_id() == id)) + .cloned() + } + fn preview_item_idx(&self) -> Option { if let Some(preview_item_id) = self.preview_item_id { self.items @@ -688,9 +694,9 @@ impl Pane { } pub fn handle_item_edit(&mut self, item_id: EntityId, cx: &AppContext) { - if let Some(preview_item_id) = self.preview_item_id { - if preview_item_id == item_id { - self.set_preview_item_id(None, cx) + if let Some(preview_item) = self.preview_item() { + if preview_item.item_id() == item_id && !preview_item.preserve_preview(cx) { + self.set_preview_item_id(None, cx); } } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 434f1fd153..db375d0d31 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2611,6 +2611,25 @@ impl Workspace { open_project_item } + pub fn is_project_item_open( + &self, + pane: &View, + project_item: &Model, + cx: &AppContext, + ) -> bool + where + T: ProjectItem, + { + use project::Item as _; + + project_item + .read(cx) + .entry_id(cx) + .and_then(|entry_id| pane.read(cx).item_for_entry(entry_id, cx)) + .and_then(|item| item.downcast::()) + .is_some() + } + pub fn open_project_item( &mut self, pane: View,