Detailed changes
@@ -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 {
<step>Add new methods 'calculate_area' and 'calculate_perimeter' to the Rectangle struct</step>
<step>Implement the 'Display' trait for the Rectangle struct</step>
-What are the operations for the step: <step>Add a new method 'calculate_area' to the Rectangle struct</step>
+What are the suggestions for the step: <step>Add a new method 'calculate_area' to the Rectangle struct</step>
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: <step>Implement the 'Display' trait for the Rectangle struct</step>
+What are the suggestions for the step: <step>Implement the 'Display' trait for the Rectangle struct</step>
A:
{
"title": "Implement Display for Rectangle",
- "operations": [
+ "suggestions": [
{
"kind": "InsertSiblingAfter",
"path": "src/shapes.rs",
@@ -109,12 +109,12 @@ impl User {
<step>Update the 'print_info' method to use formatted output</step>
<step>Remove the 'email' field from the User struct</step>
-What are the operations for the step: <step>Update the 'print_info' method to use formatted output</step>
+What are the suggestions for the step: <step>Update the 'print_info' method to use formatted output</step>
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: <step>Remove the 'email' field from the User struct</step>
+What are the suggestions for the step: <step>Remove the 'email' field from the User struct</step>
A:
{
"title": "Remove email field",
- "operations": [
+ "suggestions": [
{
"kind": "Delete",
"path": "src/user.rs",
@@ -163,12 +163,12 @@ impl Vehicle {
<step>Add a 'use std::fmt;' statement at the beginning of the file</step>
<step>Add a new method 'start_engine' in the Vehicle impl block</step>
-What are the operations for the step: <step>Add a 'use std::fmt;' statement at the beginning of the file</step>
+What are the suggestions for the step: <step>Add a 'use std::fmt;' statement at the beginning of the file</step>
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: <step>Add a new method 'start_engine' in the Vehicle impl block</step>
+What are the suggestions for the step: <step>Add a new method 'start_engine' in the Vehicle impl block</step>
A:
{
"title": "Add start_engine method",
- "operations": [
+ "suggestions": [
{
"kind": "InsertSiblingAfter",
"path": "src/vehicle.rs",
@@ -222,12 +222,12 @@ impl Employee {
<step>Make salary an f32</step>
-What are the operations for the step: <step>Make salary an f32</step>
+What are the suggestions for the step: <step>Make salary an f32</step>
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: <step>Remove the 'department' field and update the 'print_details' method</step>
+What are the correct suggestions for the step: <step>Remove the 'department' field and update the 'print_details' method</step>
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:
@@ -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<InlineAssistId>,
+struct WorkflowAssist {
editor: WeakView<Editor>,
-}
-
-#[derive(Debug, Eq, PartialEq)]
-struct ActiveWorkflowStep {
- range: Range<language::Anchor>,
- suggestions: Option<ResolvedWorkflowStepEditSuggestions>,
+ editor_was_open: bool,
+ assist_ids: Vec<InlineAssistId>,
}
pub struct ContextEditor {
@@ -1353,9 +1347,9 @@ pub struct ContextEditor {
remote_id: Option<workspace::ViewId>,
pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
pending_slash_command_blocks: HashMap<Range<language::Anchor>, CustomBlockId>,
+ workflow_assists: HashMap<Range<language::Anchor>, WorkflowAssist>,
+ active_workflow_step_range: Option<Range<language::Anchor>>,
_subscriptions: Vec<Subscription>,
- assists_by_step: HashMap<Range<language::Anchor>, StepAssists>,
- active_workflow_step: Option<ActiveWorkflowStep>,
assistant_panel: WeakView<AssistantPanel>,
error_message: Option<SharedString>,
}
@@ -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<Self>) {
- 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<Self>) -> 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<Self>) -> 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::<String>()
));
- 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<Self>) {
+ fn update_active_workflow_step_from_cursor(&mut self, cx: &mut ViewContext<Self>) {
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<language::Anchor>,
cx: &mut ViewContext<Self>,
) {
- 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<Self>) {
- 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;
- }
- }
- }
- }
+ fn activate_workflow_step(
+ &mut self,
+ step_range: Range<language::Anchor>,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<()> {
+ if self.scroll_to_existing_workflow_assist(&step_range, cx) {
+ return None;
}
- 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(),
- },
- );
+ 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 title = resolved.title.clone();
+ let suggestions = resolved.suggestions.clone();
+
+ if let Some((editor, assist_ids, editor_was_open)) = {
+ let assistant_panel = self.assistant_panel.upgrade()?;
+ if suggestions.is_empty() {
+ return None;
}
- }
- self.active_workflow_step = Some(step);
- }
+ 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::<Editor>(&active_pane, &buffer, cx);
+ workspace.open_project_item::<Editor>(active_pane, buffer, false, false, cx)
+ })
+ .log_err()?;
- fn suggest_edits(
- &mut self,
- title: String,
- edit_suggestions: HashMap<Model<Buffer>, Vec<EditSuggestionGroup>>,
- cx: &mut ViewContext<Self>,
- ) -> Option<(View<Editor>, Vec<InlineAssistId>)> {
- let assistant_panel = self.assistant_panel.upgrade()?;
- if edit_suggestions.is_empty() {
- return None;
- }
+ let (&excerpt_id, _, _) = editor
+ .read(cx)
+ .buffer()
+ .read(cx)
+ .read(cx)
+ .as_singleton()
+ .unwrap();
- 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::<Editor>(active_pane, buffer, false, false, cx)
- })
- .log_err()?;
+ // 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()
+ };
- 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 {
+ editor.set_scroll_anchor(
+ ScrollAnchor {
+ offset: gpui::Point::default(),
+ anchor,
+ },
+ cx,
+ );
+ });
+
+ 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
- .anchor_in_excerpt(excerpt_id, group.context_range.start)
- .unwrap()
- };
+ });
- editor.set_scroll_anchor(
- ScrollAnchor {
- offset: gpui::Point::default(),
- anchor,
- },
- cx,
- );
- });
+ 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 edit_suggestions {
- let excerpt_ids = multibuffer.push_excerpts(
- buffer,
- groups.iter().map(|suggestion_group| ExcerptRange {
- context: suggestion_group.context_range.clone(),
- primary: None,
- }),
+ 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,
- );
- 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()?;
+ 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<text::Anchor>,
+ cx: &'a mut ViewContext<ContextEditor>,
+ ) -> 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<language::Anchor>,
+ cx: &mut ViewContext<Self>,
+ ) -> 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<Self>) -> 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",
@@ -348,37 +348,44 @@ pub struct SlashCommandId(clock::Lamport);
#[derive(Debug)]
pub struct WorkflowStep {
pub tagged_range: Range<language::Anchor>,
- 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<Model<Buffer>, Vec<EditSuggestionGroup>>,
+ pub suggestions: HashMap<Model<Buffer>, Vec<WorkflowSuggestionGroup>>,
}
-pub enum WorkflowStepEditSuggestions {
+pub enum WorkflowStepStatus {
Pending(Task<Option<()>>),
- 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<language::Anchor>,
- pub suggestions: Vec<EditSuggestion>,
+ pub suggestions: Vec<WorkflowSuggestion>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
-pub enum EditSuggestion {
+pub enum WorkflowSuggestion {
Update {
range: Range<language::Anchor>,
description: String,
@@ -407,40 +414,40 @@ pub enum EditSuggestion {
},
}
-impl EditSuggestion {
+impl WorkflowSuggestion {
pub fn range(&self) -> Range<language::Anchor> {
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<language::Anchor>,
project: Model<Project>,
@@ -1265,13 +1270,13 @@ impl Context {
});
// Invoke the model to get its edit suggestions for this workflow step.
- let step_suggestions = model
- .use_tool::<tool::WorkflowStepEditSuggestions>(request, &cx)
+ let resolution = model
+ .use_tool::<tool::WorkflowStepResolution>(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::<EditSuggestionGroup>::new();
+ let mut suggestion_groups = Vec::<WorkflowSuggestionGroup>::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<EditSuggestion>,
+ pub suggestions: Vec<WorkflowSuggestion>,
}
- 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<Project>,
mut cx: AsyncAppContext,
- ) -> Result<(Model<Buffer>, super::EditSuggestion)> {
+ ) -> Result<(Model<Buffer>, 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<InitialInsertion> {
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,
}
}
@@ -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);
}
});
}
@@ -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 {
@@ -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<bool>,
reload_task: Option<Task<Result<()>>>,
@@ -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)]
@@ -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, |_| {});
@@ -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<Self>) {
+ 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()
@@ -287,6 +287,10 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<Point<Pixels>> {
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<Point<Pixels>>;
fn downgrade_item(&self) -> Box<dyn WeakItemHandle>;
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<T: Item> ItemHandle for View<T> {
) -> Option<Box<dyn SerializableItemHandle>> {
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<Box<dyn ItemHandle>> for AnyView {
@@ -665,6 +665,12 @@ impl Pane {
self.preview_item_id
}
+ pub fn preview_item(&self) -> Option<Box<dyn ItemHandle>> {
+ self.preview_item_id
+ .and_then(|id| self.items.iter().find(|item| item.item_id() == id))
+ .cloned()
+ }
+
fn preview_item_idx(&self) -> Option<usize> {
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);
}
}
}
@@ -2611,6 +2611,25 @@ impl Workspace {
open_project_item
}
+ pub fn is_project_item_open<T>(
+ &self,
+ pane: &View<Pane>,
+ project_item: &Model<T::Item>,
+ 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::<T>())
+ .is_some()
+ }
+
pub fn open_project_item<T>(
&mut self,
pane: View<Pane>,