Start on adding support for editing via the assistant panel (#14795)

Antonio Scandurra and Nathan created

Note that this shouldn't have any visible user-facing behavior yet. The
feature is incomplete but we wanna merge early to avoid a long-running
branch.

Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>

Change summary

Cargo.lock                                            |  24 
assets/prompts/operations.md                          | 241 +++
crates/activity_indicator/src/activity_indicator.rs   |   1 
crates/assistant/Cargo.toml                           |   3 
crates/assistant/src/assistant.rs                     |   5 
crates/assistant/src/assistant_panel.rs               | 580 +++++---
crates/assistant/src/completion_provider.rs           |  55 
crates/assistant/src/completion_provider/anthropic.rs |   2 
crates/assistant/src/completion_provider/cloud.rs     |   2 
crates/assistant/src/completion_provider/fake.rs      |  17 
crates/assistant/src/completion_provider/ollama.rs    |   2 
crates/assistant/src/completion_provider/open_ai.rs   |   2 
crates/assistant/src/context.rs                       | 873 ++++++++++--
crates/assistant/src/inline_assistant.rs              | 328 +++-
crates/assistant/src/prompt_library.rs                |  16 
crates/assistant/src/search.rs                        | 171 --
crates/assistant/src/terminal_inline_assistant.rs     |   6 
crates/auto_update/src/auto_update.rs                 |   7 
crates/collab/src/tests/following_tests.rs            |   8 
crates/command_palette/src/command_palette.rs         |   2 
crates/diagnostics/src/diagnostics.rs                 |   4 
crates/diagnostics/src/grouped_diagnostics.rs         |   4 
crates/editor/src/editor.rs                           |  50 
crates/editor/src/editor_tests.rs                     |   7 
crates/editor/src/rust_analyzer_ext.rs                |   1 
crates/extensions_ui/src/extensions_ui.rs             |   4 
crates/gpui/src/window.rs                             |  23 
crates/language/Cargo.toml                            |   1 
crates/language/src/buffer.rs                         |  70 
crates/language/src/buffer_tests.rs                   |   3 
crates/language/src/language.rs                       |   8 
crates/language/src/outline.rs                        |   1 
crates/language_tools/src/lsp_log.rs                  |   1 
crates/languages/src/rust/outline.scm                 |   3 
crates/multi_buffer/src/multi_buffer.rs               |  12 
crates/project/src/project.rs                         |  31 
crates/project_symbols/src/project_symbols.rs         |   3 
crates/search/src/project_search.rs                   |   7 
crates/tasks_ui/src/lib.rs                            |   2 
crates/terminal_view/src/terminal_view.rs             |   2 
crates/text/src/anchor.rs                             |   5 
crates/welcome/src/welcome.rs                         |   2 
crates/workspace/src/workspace.rs                     |  79 
crates/zed/src/zed.rs                                 |   7 
44 files changed, 1,860 insertions(+), 815 deletions(-)

Detailed changes

Cargo.lock ๐Ÿ”—

@@ -372,6 +372,7 @@ version = "0.1.0"
 dependencies = [
  "anthropic",
  "anyhow",
+ "assets",
  "assistant_slash_command",
  "async-watch",
  "breadcrumbs",
@@ -408,6 +409,7 @@ dependencies = [
  "rand 0.8.5",
  "regex",
  "rope",
+ "roxmltree 0.20.0",
  "schemars",
  "search",
  "semantic_index",
@@ -416,7 +418,6 @@ dependencies = [
  "settings",
  "similar",
  "smol",
- "strsim 0.11.1",
  "strum",
  "telemetry_events",
  "terminal",
@@ -2244,7 +2245,7 @@ dependencies = [
  "bitflags 1.3.2",
  "clap_lex 0.2.4",
  "indexmap 1.9.3",
- "strsim 0.10.0",
+ "strsim",
  "termcolor",
  "textwrap",
 ]
@@ -2268,7 +2269,7 @@ dependencies = [
  "anstream",
  "anstyle",
  "clap_lex 0.5.1",
- "strsim 0.10.0",
+ "strsim",
 ]
 
 [[package]]
@@ -4272,7 +4273,7 @@ version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6a595cb550439a117696039dfc69830492058211b771a2a165379f2a1a53d84d"
 dependencies = [
- "roxmltree",
+ "roxmltree 0.19.0",
 ]
 
 [[package]]
@@ -5944,6 +5945,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "async-trait",
+ "async-watch",
  "clock",
  "collections",
  "ctor",
@@ -8901,6 +8903,12 @@ version = "0.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
 
+[[package]]
+name = "roxmltree"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
+
 [[package]]
 name = "rpc"
 version = "0.1.0"
@@ -10359,12 +10367,6 @@ version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
 
-[[package]]
-name = "strsim"
-version = "0.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
-
 [[package]]
 name = "strum"
 version = "0.25.0"
@@ -11899,7 +11901,7 @@ dependencies = [
  "kurbo",
  "log",
  "pico-args",
- "roxmltree",
+ "roxmltree 0.19.0",
  "simplecss",
  "siphasher 1.0.1",
  "strict-num",

assets/prompts/operations.md ๐Ÿ”—

@@ -0,0 +1,241 @@
+Your task is to map a step from the conversation above to operations on symbols inside the provided source files.
+
+Guidelines:
+- There's no need to describe *what* to do, just *where* to do it.
+- If creating a file, assume any subsequent updates are included at the time of creation.
+- 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.
+- 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.
+- 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.
+
+The available operation types are:
+
+1. <update>: Modify an existing symbol in a file.
+2. <create_file>: Create a new file.
+3. <insert_sibling_after>: Add a new symbol as sibling after an existing symbol in a file.
+4. <append_child>: Add a new symbol as the last child of an existing symbol in a file.
+5. <prepend_child>: Add a new symbol as the first child of an existing symbol in a file.
+6. <delete>: Remove an existing symbol from a file. The `description` attribute is invalid for delete, but required for other ops.
+
+All operations *require* a path.
+Operations that *require* a symbol: <update>, <insert_sibling_after>, <delete>
+Operations that don't allow a symbol: <create>
+Operations that have an *optional* symbol: <prepend_child>, <append_child>
+
+Example 1:
+
+User:
+  ```rs src/rectangle.rs
+  struct Rectangle {
+      width: f64,
+      height: f64,
+  }
+
+  impl Rectangle {
+      fn new(width: f64, height: f64) -> Self {
+          Rectangle { width, height }
+      }
+  }
+  ```
+
+  Symbols for src/rectangle.rs:
+  - struct Rectangle
+  - impl Rectangle
+  - impl Rectangle fn new
+
+  <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>
+
+Assistant (wrong):
+<operations>
+    <append_child path="src/shapes.rs" symbol="impl Rectangle" description="Add calculate_area method" />
+    <append_child path="src/shapes.rs" symbol="impl Rectangle" description="Add calculate_perimeter method" />
+</operations>
+
+This demonstrates what NOT to do. NEVER append multiple children at the same location.
+
+Assistant (corrected):
+<operations>
+    <append_child path="src/shapes.rs" symbol="impl Rectangle" description="Add calculate area and perimeter methods" />
+</operations>
+
+User:
+What are the operations for the step: <step>Implement the 'Display' trait for the Rectangle struct</step>
+
+Assistant:
+<operations>
+    <insert_sibling_after path="src/shapes.rs" symbol="impl Rectangle" description="Implement Display trait for Rectangle"/>
+</operations>
+
+Example 2:
+
+User:
+```rs src/user.rs
+struct User {
+    pub name: String,
+    age: u32,
+    email: String,
+}
+
+impl User {
+    fn new(name: String, age: u32, email: String) -> Self {
+        User { name, age, email }
+    }
+
+    pub fn print_info(&self) {
+        println!("Name: {}, Age: {}, Email: {}", self.name, self.age, self.email);
+    }
+}
+```
+
+Symbols for src/user.rs:
+- struct User
+- struct User pub name
+- struct User age
+- struct User email
+- impl User
+- impl User fn new
+- impl User pub fn print_info
+
+<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>
+
+Assistant:
+<operations>
+    <update path="src/user.rs" symbol="impl User fn print_info" description="Use formatted output" />
+</operations>
+
+User:
+What are the operations for the step: <step>Remove the 'email' field from the User struct</step>
+
+Assistant:
+<operations>
+    <delete path="src/user.rs" symbol="struct User email" description="Remove the email field" />
+</operations>
+
+Example 3:
+
+User:
+```rs src/vehicle.rs
+struct Vehicle {
+    make: String,
+    model: String,
+    year: u32,
+}
+
+impl Vehicle {
+    fn new(make: String, model: String, year: u32) -> Self {
+        Vehicle { make, model, year }
+    }
+
+    fn print_year(&self) {
+        println!("Year: {}", self.year);
+    }
+}
+```
+
+Symbols for src/vehicle.rs:
+- struct Vehicle
+- struct Vehicle make
+- struct Vehicle model
+- struct Vehicle year
+- impl Vehicle
+- impl Vehicle fn new
+- impl Vehicle fn print_year
+
+<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>
+
+Assistant:
+<operations>
+    <prepend_child path="src/vehicle.rs" description="Add 'use std::fmt' statement" />
+</operations>
+
+User:
+What are the operations for the step: <step>Add a new method 'start_engine' in the Vehicle impl block</step>
+
+Assistant:
+<operations>
+    <insert_sibling_after path="src/vehicle.rs" symbol="impl Vehicle fn new" description="Add start_engine method"/>
+</operations>
+
+Example 4:
+
+User:
+```rs src/employee.rs
+struct Employee {
+    name: String,
+    position: String,
+    salary: u32,
+    department: String,
+}
+
+impl Employee {
+    fn new(name: String, position: String, salary: u32, department: String) -> Self {
+        Employee { name, position, salary, department }
+    }
+
+    fn print_details(&self) {
+        println!("Name: {}, Position: {}, Salary: {}, Department: {}",
+                  self.name, self.position, self.salary, self.department);
+    }
+
+    fn give_raise(&mut self, amount: u32) {
+        self.salary += amount;
+    }
+}
+```
+
+Symbols for src/employee.rs:
+- struct Employee
+- struct Employee name
+- struct Employee position
+- struct Employee salary
+- struct Employee department
+- impl Employee
+- impl Employee fn new
+- impl Employee fn print_details
+- impl Employee fn give_raise
+
+<step>Make salary an f32</step>
+
+What are the operations for the step: <step>Make salary an f32</step>
+
+A (wrong):
+  <operations>
+      <update path="src/employee.rs" symbol="struct Employee" description="Change the type of salary to an f32" />
+      <update path="src/employee.rs" symbol="struct Employee salary" description="Change the type to an f32" />
+  </operations>
+
+This example demonstrates what not to do. `struct Employee salary` is a child of `struct Employee`.
+
+A (corrected):
+  <operations>
+      <update path="src/employee.rs" symbol="struct Employee salary" description="Change the type to an f32" />
+  </operations>
+
+User:
+  What are the correct operations for the step: <step>Remove the 'department' field and update the 'print_details' method</step>
+
+A:
+  <operations>
+      <delete path="src/employee.rs" symbol="struct Employee department" />
+      <update path="src/employee.rs" symbol="impl Employee fn print_details" description="Don't print the 'department' field" />
+  </operations>
+
+Now generate the operations for the following step.
+Output only valid XML containing valid operations with their required attributes.
+NEVER output code or any other text inside <operation> tags. If you do, you will replaced with another model.
+Your response *MUST* begin with <operations> and end with </operations>:

crates/assistant/Cargo.toml ๐Ÿ”—

@@ -23,6 +23,7 @@ test-support = [
 [dependencies]
 anthropic = { workspace = true, features = ["schemars"] }
 anyhow.workspace = true
+assets.workspace = true
 assistant_slash_command.workspace = true
 async-watch.workspace = true
 breadcrumbs.workspace = true
@@ -63,7 +64,6 @@ serde_json.workspace = true
 settings.workspace = true
 similar.workspace = true
 smol.workspace = true
-strsim = "0.11"
 strum.workspace = true
 telemetry_events.workspace = true
 terminal.workspace = true
@@ -76,6 +76,7 @@ util.workspace = true
 uuid.workspace = true
 workspace.workspace = true
 picker.workspace = true
+roxmltree = "0.20.0"
 
 [dev-dependencies]
 ctor.workspace = true

crates/assistant/src/assistant.rs ๐Ÿ”—

@@ -7,7 +7,6 @@ mod inline_assistant;
 mod model_selector;
 mod prompt_library;
 mod prompts;
-mod search;
 mod slash_command;
 mod streaming_diff;
 mod terminal_inline_assistant;
@@ -53,9 +52,9 @@ actions!(
         InsertActivePrompt,
         DeployHistory,
         DeployPromptLibrary,
-        ApplyEdit,
         ConfirmCommand,
-        ToggleModelSelector
+        ToggleModelSelector,
+        DebugEditSteps
     ]
 );
 

crates/assistant/src/assistant_panel.rs ๐Ÿ”—

@@ -1,19 +1,19 @@
 use crate::{
     assistant_settings::{AssistantDockPosition, AssistantSettings},
-    humanize_token_count, parse_next_edit_suggestion,
+    humanize_token_count,
     prompt_library::open_prompt_library,
-    search::*,
     slash_command::{
         default_command::DefaultSlashCommand,
         docs_command::{DocsSlashCommand, DocsSlashCommandArgs},
         SlashCommandCompletionProvider, SlashCommandRegistry,
     },
     terminal_inline_assistant::TerminalInlineAssistant,
-    ApplyEdit, Assist, CompletionProvider, ConfirmCommand, Context, ContextEvent, ContextId,
-    ContextStore, CycleMessageRole, DeployHistory, DeployPromptLibrary, EditSuggestion,
-    InlineAssist, InlineAssistant, InsertIntoEditor, MessageStatus, ModelSelector,
-    PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata,
-    ResetKey, Role, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
+    Assist, CompletionProvider, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore,
+    CycleMessageRole, DebugEditSteps, DeployHistory, DeployPromptLibrary, EditStep,
+    EditStepOperations, EditSuggestionGroup, InlineAssist, InlineAssistId, InlineAssistant,
+    InsertIntoEditor, MessageStatus, ModelSelector, PendingSlashCommand, PendingSlashCommandStatus,
+    QuoteSelection, RemoteContextMetadata, ResetKey, Role, SavedContextMetadata, Split,
+    ToggleFocus, ToggleModelSelector,
 };
 use anyhow::{anyhow, Result};
 use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
@@ -25,29 +25,36 @@ use editor::{
     display_map::{
         BlockDisposition, BlockId, BlockProperties, BlockStyle, Crease, RenderBlock, ToDisplayPoint,
     },
-    scroll::{Autoscroll, AutoscrollStrategy},
-    Anchor, Editor, EditorEvent, RowExt, ToOffset as _, ToPoint,
+    scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor},
+    Anchor, Editor, EditorEvent, ExcerptRange, MultiBuffer, RowExt, ToOffset as _, ToPoint,
 };
 use editor::{display_map::CreaseId, FoldPlaceholder};
 use fs::Fs;
 use gpui::{
     div, percentage, point, Action, Animation, AnimationExt, AnyElement, AnyView, AppContext,
-    AsyncWindowContext, ClipboardItem, DismissEvent, Empty, EventEmitter, FocusHandle,
-    FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Pixels, Render,
-    SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
+    AsyncWindowContext, ClipboardItem, Context as _, DismissEvent, Empty, Entity, EventEmitter,
+    FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Pixels,
+    Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
     UpdateGlobal, View, ViewContext, VisualContext, WeakView, WindowContext,
 };
 use indexed_docs::IndexedDocsStore;
 use language::{
-    language_settings::SoftWrap, AutoindentMode, Buffer, LanguageRegistry, LspAdapterDelegate,
-    OffsetRangeExt as _, Point, ToOffset,
+    language_settings::SoftWrap, Buffer, Capability, LanguageRegistry, LspAdapterDelegate, Point,
+    ToOffset,
 };
 use multi_buffer::MultiBufferRow;
 use picker::{Picker, PickerDelegate};
-use project::{Project, ProjectLspAdapterDelegate, ProjectTransaction};
+use project::{Project, ProjectLspAdapterDelegate};
 use search::{buffer_search::DivRegistrar, BufferSearchBar};
 use settings::Settings;
-use std::{cmp, fmt::Write, ops::Range, path::PathBuf, sync::Arc, time::Duration};
+use std::{
+    cmp::{self, Ordering},
+    fmt::Write,
+    ops::Range,
+    path::PathBuf,
+    sync::Arc,
+    time::Duration,
+};
 use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
 use theme::ThemeSettings;
 use ui::{
@@ -60,7 +67,8 @@ use util::ResultExt;
 use workspace::{
     dock::{DockPosition, Panel, PanelEvent},
     item::{self, BreadcrumbText, FollowableItem, Item, ItemHandle},
-    pane,
+    notifications::NotifyTaskExt,
+    pane::{self, SaveIntent},
     searchable::{SearchEvent, SearchableItem},
     Pane, Save, ToggleZoom, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
 };
@@ -591,6 +599,7 @@ impl AssistantPanel {
             make_lsp_adapter_delegate(workspace.project(), cx).log_err()
         });
 
+        let assistant_panel = cx.view().downgrade();
         let editor = cx.new_view(|cx| {
             let mut editor = ContextEditor::for_context(
                 context,
@@ -598,6 +607,7 @@ impl AssistantPanel {
                 workspace.clone(),
                 self.project.clone(),
                 lsp_adapter_delegate,
+                assistant_panel,
                 cx,
             );
             editor.insert_default_prompt(cx);
@@ -720,6 +730,7 @@ impl AssistantPanel {
 
         cx.spawn(|this, mut cx| async move {
             let context = context.await?;
+            let assistant_panel = this.clone();
             this.update(&mut cx, |this, cx| {
                 let workspace = workspace
                     .upgrade()
@@ -731,6 +742,7 @@ impl AssistantPanel {
                         workspace,
                         project,
                         lsp_adapter_delegate,
+                        assistant_panel,
                         cx,
                     )
                 });
@@ -774,6 +786,7 @@ impl AssistantPanel {
 
         cx.spawn(|this, mut cx| async move {
             let context = context.await?;
+            let assistant_panel = this.clone();
             this.update(&mut cx, |this, cx| {
                 let workspace = workspace
                     .upgrade()
@@ -785,6 +798,7 @@ impl AssistantPanel {
                         workspace,
                         this.project.clone(),
                         lsp_adapter_delegate,
+                        assistant_panel,
                         cx,
                     )
                 });
@@ -956,10 +970,18 @@ struct ScrollPosition {
     cursor: Anchor,
 }
 
+struct ActiveEditStep {
+    start: language::Anchor,
+    assist_ids: Vec<InlineAssistId>,
+    editor: Option<WeakView<Editor>>,
+    _open_editor: Task<Result<()>>,
+}
+
 pub struct ContextEditor {
     context: Model<Context>,
     fs: Arc<dyn Fs>,
     workspace: WeakView<Workspace>,
+    project: Model<Project>,
     lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
     editor: View<Editor>,
     blocks: HashSet<BlockId>,
@@ -968,6 +990,8 @@ pub struct ContextEditor {
     pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
     pending_slash_command_blocks: HashMap<Range<language::Anchor>, BlockId>,
     _subscriptions: Vec<Subscription>,
+    active_edit_step: Option<ActiveEditStep>,
+    assistant_panel: WeakView<AssistantPanel>,
 }
 
 impl ContextEditor {
@@ -979,6 +1003,7 @@ impl ContextEditor {
         workspace: View<Workspace>,
         project: Model<Project>,
         lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
+        assistant_panel: WeakView<AssistantPanel>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let completion_provider = SlashCommandCompletionProvider::new(
@@ -996,7 +1021,7 @@ impl ContextEditor {
             editor.set_show_wrap_guides(false, cx);
             editor.set_show_indent_guides(false, cx);
             editor.set_completion_provider(Box::new(completion_provider));
-            editor.set_collaboration_hub(Box::new(project));
+            editor.set_collaboration_hub(Box::new(project.clone()));
             editor
         });
 
@@ -1017,9 +1042,12 @@ impl ContextEditor {
             remote_id: None,
             fs,
             workspace: workspace.downgrade(),
+            project,
             pending_slash_command_creases: HashMap::default(),
             pending_slash_command_blocks: HashMap::default(),
             _subscriptions,
+            active_edit_step: None,
+            assistant_panel,
         };
         this.update_message_headers(cx);
         this.insert_slash_command_output_sections(sections, cx);
@@ -1052,31 +1080,37 @@ impl ContextEditor {
     }
 
     fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
-        let cursors = self.cursors(cx);
+        if !self.apply_edit_step(cx) {
+            self.send_to_model(cx);
+        }
+    }
 
-        let user_messages = self.context.update(cx, |context, cx| {
-            let selected_messages = context
-                .messages_for_offsets(cursors, cx)
-                .into_iter()
-                .map(|message| message.id)
-                .collect();
-            context.assist(selected_messages, cx)
-        });
-        let new_selections = user_messages
-            .iter()
-            .map(|message| {
-                let cursor = message
+    fn apply_edit_step(&mut self, cx: &mut ViewContext<Self>) -> bool {
+        if let Some(step) = self.active_edit_step.as_ref() {
+            InlineAssistant::update_global(cx, |assistant, cx| {
+                for assist_id in &step.assist_ids {
+                    assistant.start_assist(*assist_id, cx);
+                }
+                !step.assist_ids.is_empty()
+            })
+        } else {
+            false
+        }
+    }
+
+    fn send_to_model(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) {
+            let new_selection = {
+                let cursor = user_message
                     .start
                     .to_offset(self.context.read(cx).buffer().read(cx));
                 cursor..cursor
-            })
-            .collect::<Vec<_>>();
-        if !new_selections.is_empty() {
+            };
             self.editor.update(cx, |editor, cx| {
                 editor.change_selections(
                     Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
                     cx,
-                    |selections| selections.select_ranges(new_selections),
+                    |selections| selections.select_ranges([new_selection]),
                 );
             });
             // Avoid scrolling to the new cursor position so the assistant's output is stable.
@@ -1093,6 +1127,53 @@ impl ContextEditor {
         }
     }
 
+    fn debug_edit_steps(&mut self, _: &DebugEditSteps, cx: &mut ViewContext<Self>) {
+        let mut output = String::new();
+        for (i, step) in self.context.read(cx).edit_steps().iter().enumerate() {
+            output.push_str(&format!("Step {}:\n", i + 1));
+            output.push_str(&format!(
+                "Content: {}\n",
+                self.context
+                    .read(cx)
+                    .buffer()
+                    .read(cx)
+                    .text_for_range(step.source_range.clone())
+                    .collect::<String>()
+            ));
+            match &step.operations {
+                Some(EditStepOperations::Parsed {
+                    operations,
+                    raw_output,
+                }) => {
+                    output.push_str(&format!("Raw Output:\n{raw_output}\n"));
+                    output.push_str("Parsed Operations:\n");
+                    for op in operations {
+                        output.push_str(&format!("  {:?}\n", op));
+                    }
+                }
+                Some(EditStepOperations::Pending(_)) => {
+                    output.push_str("Operations: Pending\n");
+                }
+                None => {
+                    output.push_str("Operations: None\n");
+                }
+            }
+            output.push('\n');
+        }
+
+        let editor = self
+            .workspace
+            .update(cx, |workspace, cx| Editor::new_in_workspace(workspace, cx));
+
+        if let Ok(editor) = editor {
+            cx.spawn(|_, mut cx| async move {
+                let editor = editor.await?;
+                editor.update(&mut cx, |editor, cx| editor.set_text(output, cx))
+            })
+            .detach_and_notify_err(cx);
+        }
+    }
+
     fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext<Self>) {
         let cursors = self.cursors(cx);
         self.context.update(cx, |context, cx| {
@@ -1222,39 +1303,8 @@ impl ContextEditor {
                     context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
                 });
             }
-            ContextEvent::EditSuggestionsChanged => {
-                self.editor.update(cx, |editor, cx| {
-                    let buffer = editor.buffer().read(cx).snapshot(cx);
-                    let excerpt_id = *buffer.as_singleton().unwrap().0;
-                    let context = self.context.read(cx);
-                    let highlighted_rows = context
-                        .edit_suggestions()
-                        .iter()
-                        .map(|suggestion| {
-                            let start = buffer
-                                .anchor_in_excerpt(excerpt_id, suggestion.source_range.start)
-                                .unwrap();
-                            let end = buffer
-                                .anchor_in_excerpt(excerpt_id, suggestion.source_range.end)
-                                .unwrap();
-                            start..=end
-                        })
-                        .collect::<Vec<_>>();
-
-                    editor.clear_row_highlights::<EditSuggestion>();
-                    for range in highlighted_rows {
-                        editor.highlight_rows::<EditSuggestion>(
-                            range,
-                            Some(
-                                cx.theme()
-                                    .colors()
-                                    .editor_document_highlight_read_background,
-                            ),
-                            false,
-                            cx,
-                        );
-                    }
-                });
+            ContextEvent::EditStepsChanged => {
+                cx.notify();
             }
             ContextEvent::SummaryChanged => {
                 cx.emit(EditorEvent::TitleChanged);
@@ -1515,12 +1565,200 @@ impl ContextEditor {
             }
             EditorEvent::SelectionsChanged { .. } => {
                 self.scroll_position = self.cursor_scroll_position(cx);
+                if self
+                    .edit_step_for_cursor(cx)
+                    .map(|step| step.source_range.start)
+                    != self.active_edit_step.as_ref().map(|step| step.start)
+                {
+                    if let Some(old_active_edit_step) = self.active_edit_step.take() {
+                        if let Some(editor) = old_active_edit_step
+                            .editor
+                            .and_then(|editor| editor.upgrade())
+                        {
+                            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) {
+                                                pane.close_item_by_id(
+                                                    item_id,
+                                                    SaveIntent::Skip,
+                                                    cx,
+                                                )
+                                                .detach_and_log_err(cx);
+                                            }
+                                        });
+                                    }
+                                })
+                                .ok();
+                        }
+                    }
+
+                    if let Some(new_active_step) = self.edit_step_for_cursor(cx) {
+                        let suggestions = new_active_step.edit_suggestions(&self.project, cx);
+                        self.active_edit_step = Some(ActiveEditStep {
+                            start: new_active_step.source_range.start,
+                            assist_ids: Vec::new(),
+                            editor: None,
+                            _open_editor: self.open_editor_for_edit_suggestions(suggestions, cx),
+                        });
+                    }
+                }
             }
             _ => {}
         }
         cx.emit(event.clone());
     }
 
+    fn open_editor_for_edit_suggestions(
+        &mut self,
+        edit_suggestions: Task<HashMap<Model<Buffer>, Vec<EditSuggestionGroup>>>,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        let workspace = self.workspace.clone();
+        let project = self.project.clone();
+        let assistant_panel = self.assistant_panel.clone();
+        cx.spawn(|this, mut cx| async move {
+            let edit_suggestions = edit_suggestions.await;
+
+            let mut assist_ids = Vec::new();
+            let editor = if edit_suggestions.is_empty() {
+                return Ok(());
+            } else 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, suggestion_groups) = edit_suggestions.into_iter().next().unwrap();
+                let suggestion_group = suggestion_groups.into_iter().next().unwrap();
+                let editor = workspace.update(&mut cx, |workspace, cx| {
+                    let active_pane = workspace.active_pane().clone();
+                    workspace.open_project_item::<Editor>(active_pane, buffer, false, false, cx)
+                })?;
+
+                cx.update(|cx| {
+                    for suggestion in suggestion_group.suggestions {
+                        let description = suggestion.description.unwrap_or_else(|| "Delete".into());
+                        let range = {
+                            let buffer = editor.read(cx).buffer().read(cx).read(cx);
+                            let (&excerpt_id, _, _) = buffer.as_singleton().unwrap();
+                            buffer
+                                .anchor_in_excerpt(excerpt_id, suggestion.range.start)
+                                .unwrap()
+                                ..buffer
+                                    .anchor_in_excerpt(excerpt_id, suggestion.range.end)
+                                    .unwrap()
+                        };
+                        let initial_text = suggestion.prepend_newline.then(|| "\n".into());
+                        InlineAssistant::update_global(cx, |assistant, cx| {
+                            assist_ids.push(assistant.suggest_assist(
+                                &editor,
+                                range,
+                                description,
+                                initial_text,
+                                Some(workspace.clone()),
+                                assistant_panel.upgrade().as_ref(),
+                                cx,
+                            ));
+                        });
+                    }
+
+                    // Scroll the editor to the suggested assist
+                    editor.update(cx, |editor, cx| {
+                        let anchor = {
+                            let buffer = editor.buffer().read(cx).read(cx);
+                            let (&excerpt_id, _, _) = buffer.as_singleton().unwrap();
+                            buffer
+                                .anchor_in_excerpt(excerpt_id, suggestion_group.context_range.start)
+                                .unwrap()
+                        };
+
+                        editor.set_scroll_anchor(
+                            ScrollAnchor {
+                                offset: gpui::Point::default(),
+                                anchor,
+                            },
+                            cx,
+                        );
+                    });
+                })?;
+
+                editor
+            } else {
+                // If there are multiple buffers or suggestion groups, create a multibuffer
+                let mut inline_assist_suggestions = Vec::new();
+                let multibuffer = cx.new_model(|cx| {
+                    let replica_id = project.read(cx).replica_id();
+                    let mut multibuffer = MultiBuffer::new(replica_id, Capability::ReadWrite);
+                    for (buffer, suggestion_groups) in edit_suggestions {
+                        let excerpt_ids = multibuffer.push_excerpts(
+                            buffer,
+                            suggestion_groups
+                                .iter()
+                                .map(|suggestion_group| ExcerptRange {
+                                    context: suggestion_group.context_range.clone(),
+                                    primary: None,
+                                }),
+                            cx,
+                        );
+
+                        for (excerpt_id, suggestion_group) in
+                            excerpt_ids.into_iter().zip(suggestion_groups)
+                        {
+                            for suggestion in suggestion_group.suggestions {
+                                let description =
+                                    suggestion.description.unwrap_or_else(|| "Delete".into());
+                                let range = {
+                                    let multibuffer = multibuffer.read(cx);
+                                    multibuffer
+                                        .anchor_in_excerpt(excerpt_id, suggestion.range.start)
+                                        .unwrap()
+                                        ..multibuffer
+                                            .anchor_in_excerpt(excerpt_id, suggestion.range.end)
+                                            .unwrap()
+                                };
+                                let initial_text =
+                                    suggestion.prepend_newline.then(|| "\n".to_string());
+                                inline_assist_suggestions.push((range, description, initial_text));
+                            }
+                        }
+                    }
+                    multibuffer
+                })?;
+
+                let editor = cx
+                    .new_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), true, cx))?;
+                cx.update(|cx| {
+                    InlineAssistant::update_global(cx, |assistant, cx| {
+                        for (range, description, initial_text) in inline_assist_suggestions {
+                            assist_ids.push(assistant.suggest_assist(
+                                &editor,
+                                range,
+                                description,
+                                initial_text,
+                                Some(workspace.clone()),
+                                assistant_panel.upgrade().as_ref(),
+                                cx,
+                            ));
+                        }
+                    })
+                })?;
+                workspace.update(&mut cx, |workspace, cx| {
+                    workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx)
+                })?;
+
+                editor
+            };
+
+            this.update(&mut cx, |this, _cx| {
+                if let Some(step) = this.active_edit_step.as_mut() {
+                    step.assist_ids = assist_ids;
+                    step.editor = Some(editor.downgrade());
+                }
+            })
+        })
+    }
+
     fn handle_editor_search_event(
         &mut self,
         _: View<Editor>,
@@ -1785,173 +2023,6 @@ impl ContextEditor {
         });
     }
 
-    fn apply_edit(&mut self, _: &ApplyEdit, cx: &mut ViewContext<Self>) {
-        let Some(workspace) = self.workspace.upgrade() else {
-            return;
-        };
-        let project = workspace.read(cx).project().clone();
-
-        struct Edit {
-            old_text: String,
-            new_text: String,
-        }
-
-        let context = self.context.read(cx);
-        let context_buffer = context.buffer().read(cx);
-        let context_buffer_snapshot = context_buffer.snapshot();
-
-        let selections = self.editor.read(cx).selections.disjoint_anchors();
-        let mut selections = selections.iter().peekable();
-        let selected_suggestions = context
-            .edit_suggestions()
-            .iter()
-            .filter(|suggestion| {
-                while let Some(selection) = selections.peek() {
-                    if selection
-                        .end
-                        .text_anchor
-                        .cmp(&suggestion.source_range.start, context_buffer)
-                        .is_lt()
-                    {
-                        selections.next();
-                        continue;
-                    }
-                    if selection
-                        .start
-                        .text_anchor
-                        .cmp(&suggestion.source_range.end, context_buffer)
-                        .is_gt()
-                    {
-                        break;
-                    }
-                    return true;
-                }
-                false
-            })
-            .cloned()
-            .collect::<Vec<_>>();
-
-        let mut opened_buffers: HashMap<PathBuf, Task<Result<Model<Buffer>>>> = HashMap::default();
-        project.update(cx, |project, cx| {
-            for suggestion in &selected_suggestions {
-                opened_buffers
-                    .entry(suggestion.full_path.clone())
-                    .or_insert_with(|| {
-                        project.open_buffer_for_full_path(&suggestion.full_path, cx)
-                    });
-            }
-        });
-
-        cx.spawn(|this, mut cx| async move {
-            let mut buffers_by_full_path = HashMap::default();
-            for (full_path, buffer) in opened_buffers {
-                if let Some(buffer) = buffer.await.log_err() {
-                    buffers_by_full_path.insert(full_path, buffer);
-                }
-            }
-
-            let mut suggestions_by_buffer = HashMap::default();
-            cx.update(|cx| {
-                for suggestion in selected_suggestions {
-                    if let Some(buffer) = buffers_by_full_path.get(&suggestion.full_path) {
-                        let (_, edits) = suggestions_by_buffer
-                            .entry(buffer.clone())
-                            .or_insert_with(|| (buffer.read(cx).snapshot(), Vec::new()));
-
-                        let mut lines = context_buffer_snapshot
-                            .as_rope()
-                            .chunks_in_range(
-                                suggestion.source_range.to_offset(&context_buffer_snapshot),
-                            )
-                            .lines();
-                        if let Some(suggestion) = parse_next_edit_suggestion(&mut lines) {
-                            let old_text = context_buffer_snapshot
-                                .text_for_range(suggestion.old_text_range)
-                                .collect();
-                            let new_text = context_buffer_snapshot
-                                .text_for_range(suggestion.new_text_range)
-                                .collect();
-                            edits.push(Edit { old_text, new_text });
-                        }
-                    }
-                }
-            })?;
-
-            let edits_by_buffer = cx
-                .background_executor()
-                .spawn(async move {
-                    let mut result = HashMap::default();
-                    for (buffer, (snapshot, suggestions)) in suggestions_by_buffer {
-                        let edits =
-                            result
-                                .entry(buffer)
-                                .or_insert(Vec::<(Range<language::Anchor>, _)>::new());
-                        for suggestion in suggestions {
-                            if let Some(range) =
-                                fuzzy_search_lines(snapshot.as_rope(), &suggestion.old_text)
-                            {
-                                let edit_start = snapshot.anchor_after(range.start);
-                                let edit_end = snapshot.anchor_before(range.end);
-                                if let Err(ix) = edits.binary_search_by(|(range, _)| {
-                                    range.start.cmp(&edit_start, &snapshot)
-                                }) {
-                                    edits.insert(
-                                        ix,
-                                        (edit_start..edit_end, suggestion.new_text.clone()),
-                                    );
-                                }
-                            } else {
-                                log::info!(
-                                    "assistant edit did not match any text in buffer {:?}",
-                                    &suggestion.old_text
-                                );
-                            }
-                        }
-                    }
-                    result
-                })
-                .await;
-
-            let mut project_transaction = ProjectTransaction::default();
-            let (editor, workspace, title) = this.update(&mut cx, |this, cx| {
-                for (buffer_handle, edits) in edits_by_buffer {
-                    buffer_handle.update(cx, |buffer, cx| {
-                        buffer.start_transaction();
-                        buffer.edit(
-                            edits,
-                            Some(AutoindentMode::Block {
-                                original_indent_columns: Vec::new(),
-                            }),
-                            cx,
-                        );
-                        buffer.end_transaction(cx);
-                        if let Some(transaction) = buffer.finalize_last_transaction() {
-                            project_transaction
-                                .0
-                                .insert(buffer_handle.clone(), transaction.clone());
-                        }
-                    });
-                }
-
-                (
-                    this.editor.downgrade(),
-                    this.workspace.clone(),
-                    this.title(cx),
-                )
-            })?;
-
-            Editor::open_project_transaction(
-                &editor,
-                workspace,
-                project_transaction,
-                format!("Edits from {}", title),
-                cx,
-            )
-            .await
-        })
-        .detach_and_log_err(cx);
-    }
-
     fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
         self.context
             .update(cx, |context, cx| context.save(None, self.fs.clone(), cx));
@@ -1967,6 +2038,14 @@ 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.edit_step_for_cursor(cx) {
+            Some(edit_step) => match &edit_step.operations {
+                Some(EditStepOperations::Pending(_)) => "Computing Changes...",
+                Some(EditStepOperations::Parsed { .. }) => "Apply Changes",
+                None => "Send",
+            },
+            None => "Send",
+        };
         ButtonLike::new("send_button")
             .style(ButtonStyle::Filled)
             .layer(ElevationIndex::ModalSurface)
@@ -1974,11 +2053,38 @@ impl ContextEditor {
                 KeyBinding::for_action_in(&Assist, &focus_handle, cx)
                     .map(|binding| binding.into_any_element()),
             )
-            .child(Label::new("Send"))
+            .child(Label::new(button_text))
             .on_click(move |_event, cx| {
                 focus_handle.dispatch_action(&Assist, cx);
             })
     }
+
+    fn edit_step_for_cursor<'a>(&'a self, cx: &'a AppContext) -> Option<&'a EditStep> {
+        let newest_cursor = self
+            .editor
+            .read(cx)
+            .selections
+            .newest_anchor()
+            .head()
+            .text_anchor;
+        let context = self.context.read(cx);
+        let buffer = context.buffer().read(cx);
+
+        let edit_steps = context.edit_steps();
+        edit_steps
+            .binary_search_by(|step| {
+                let step_range = step.source_range.clone();
+                if newest_cursor.cmp(&step_range.start, buffer).is_lt() {
+                    Ordering::Greater
+                } else if newest_cursor.cmp(&step_range.end, buffer).is_gt() {
+                    Ordering::Less
+                } else {
+                    Ordering::Equal
+                }
+            })
+            .ok()
+            .map(|index| &edit_steps[index])
+    }
 }
 
 impl EventEmitter<EditorEvent> for ContextEditor {}
@@ -1995,7 +2101,7 @@ impl Render for ContextEditor {
             .capture_action(cx.listener(ContextEditor::confirm_command))
             .on_action(cx.listener(ContextEditor::assist))
             .on_action(cx.listener(ContextEditor::split))
-            .on_action(cx.listener(ContextEditor::apply_edit))
+            .on_action(cx.listener(ContextEditor::debug_edit_steps))
             .size_full()
             .v_flex()
             .child(

crates/assistant/src/completion_provider.rs ๐Ÿ”—

@@ -20,11 +20,10 @@ use crate::{
 };
 use anyhow::Result;
 use client::Client;
-use futures::{future::BoxFuture, stream::BoxStream};
+use futures::{future::BoxFuture, stream::BoxStream, StreamExt};
 use gpui::{AnyView, AppContext, BorrowAppContext, Task, WindowContext};
 use settings::{Settings, SettingsStore};
-use std::time::Duration;
-use std::{any::Any, sync::Arc};
+use std::{any::Any, pin::Pin, sync::Arc, task::Poll, time::Duration};
 
 /// Choose which model to use for openai provider.
 /// If the model is not available, try to use the first available model, or fallback to the original model.
@@ -55,10 +54,21 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
 }
 
 pub struct CompletionResponse {
-    pub inner: BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>,
+    inner: BoxStream<'static, Result<String>>,
     _lock: SemaphoreGuardArc,
 }
 
+impl futures::Stream for CompletionResponse {
+    type Item = Result<String>;
+
+    fn poll_next(
+        mut self: Pin<&mut Self>,
+        cx: &mut std::task::Context<'_>,
+    ) -> Poll<Option<Self::Item>> {
+        Pin::new(&mut self.inner).poll_next(cx)
+    }
+}
+
 pub trait LanguageModelCompletionProvider: Send + Sync {
     fn available_models(&self, cx: &AppContext) -> Vec<LanguageModel>;
     fn settings_version(&self) -> usize;
@@ -72,7 +82,7 @@ pub trait LanguageModelCompletionProvider: Send + Sync {
         request: LanguageModelRequest,
         cx: &AppContext,
     ) -> BoxFuture<'static, Result<usize>>;
-    fn complete(
+    fn stream_completion(
         &self,
         request: LanguageModelRequest,
     ) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>;
@@ -136,20 +146,34 @@ impl CompletionProvider {
         self.provider.read().count_tokens(request, cx)
     }
 
-    pub fn complete(
+    pub fn stream_completion(
         &self,
         request: LanguageModelRequest,
         cx: &AppContext,
-    ) -> Task<CompletionResponse> {
+    ) -> Task<Result<CompletionResponse>> {
         let rate_limiter = self.request_limiter.clone();
         let provider = self.provider.clone();
-        cx.background_executor().spawn(async move {
+        cx.foreground_executor().spawn(async move {
             let lock = rate_limiter.acquire_arc().await;
-            let response = provider.read().complete(request);
-            CompletionResponse {
+            let response = provider.read().stream_completion(request);
+            let response = response.await?;
+            Ok(CompletionResponse {
                 inner: response,
                 _lock: lock,
+            })
+        })
+    }
+
+    pub fn complete(&self, request: LanguageModelRequest, cx: &AppContext) -> Task<Result<String>> {
+        let response = self.stream_completion(request, cx);
+        cx.foreground_executor().spawn(async move {
+            let mut chunks = response.await?;
+            let mut completion = String::new();
+            while let Some(chunk) = chunks.next().await {
+                let chunk = chunk?;
+                completion.push_str(&chunk);
             }
+            Ok(completion)
         })
     }
 }
@@ -300,7 +324,7 @@ mod tests {
 
         // Enqueue some requests
         for i in 0..MAX_CONCURRENT_COMPLETION_REQUESTS * 2 {
-            let response = provider.complete(
+            let response = provider.stream_completion(
                 LanguageModelRequest {
                     temperature: i as f32 / 10.0,
                     ..Default::default()
@@ -309,8 +333,7 @@ mod tests {
             );
             cx.background_executor()
                 .spawn(async move {
-                    let response = response.await;
-                    let mut stream = response.inner.await.unwrap();
+                    let mut stream = response.await.unwrap();
                     while let Some(message) = stream.next().await {
                         message.unwrap();
                     }
@@ -326,7 +349,7 @@ mod tests {
 
         // Get the first completion request that is in flight and mark it as completed.
         let completion = fake_provider
-            .running_completions()
+            .pending_completions()
             .into_iter()
             .next()
             .unwrap();
@@ -347,7 +370,7 @@ mod tests {
         );
 
         // Mark all completion requests as finished that are in flight.
-        for request in fake_provider.running_completions() {
+        for request in fake_provider.pending_completions() {
             fake_provider.finish_completion(&request);
         }
 
@@ -362,7 +385,7 @@ mod tests {
         );
 
         // Finish all remaining completion requests.
-        for request in fake_provider.running_completions() {
+        for request in fake_provider.pending_completions() {
             fake_provider.finish_completion(&request);
         }
 

crates/assistant/src/completion_provider/anthropic.rs ๐Ÿ”—

@@ -94,7 +94,7 @@ impl LanguageModelCompletionProvider for AnthropicCompletionProvider {
         count_open_ai_tokens(request, cx.background_executor())
     }
 
-    fn complete(
+    fn stream_completion(
         &self,
         request: LanguageModelRequest,
     ) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {

crates/assistant/src/completion_provider/cloud.rs ๐Ÿ”—

@@ -135,7 +135,7 @@ impl LanguageModelCompletionProvider for CloudCompletionProvider {
         }
     }
 
-    fn complete(
+    fn stream_completion(
         &self,
         mut request: LanguageModelRequest,
     ) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {

crates/assistant/src/completion_provider/fake.rs ๐Ÿ”—

@@ -23,7 +23,7 @@ impl FakeCompletionProvider {
         this
     }
 
-    pub fn running_completions(&self) -> Vec<LanguageModelRequest> {
+    pub fn pending_completions(&self) -> Vec<LanguageModelRequest> {
         self.current_completion_txs
             .lock()
             .keys()
@@ -35,7 +35,7 @@ impl FakeCompletionProvider {
         self.current_completion_txs.lock().len()
     }
 
-    pub fn send_completion(&self, request: &LanguageModelRequest, chunk: String) {
+    pub fn send_completion_chunk(&self, request: &LanguageModelRequest, chunk: String) {
         let json = serde_json::to_string(request).unwrap();
         self.current_completion_txs
             .lock()
@@ -45,10 +45,19 @@ impl FakeCompletionProvider {
             .unwrap();
     }
 
+    pub fn send_last_completion_chunk(&self, chunk: String) {
+        self.send_completion_chunk(self.pending_completions().last().unwrap(), chunk);
+    }
+
     pub fn finish_completion(&self, request: &LanguageModelRequest) {
         self.current_completion_txs
             .lock()
-            .remove(&serde_json::to_string(request).unwrap());
+            .remove(&serde_json::to_string(request).unwrap())
+            .unwrap();
+    }
+
+    pub fn finish_last_completion(&self) {
+        self.finish_completion(self.pending_completions().last().unwrap());
     }
 }
 
@@ -89,7 +98,7 @@ impl LanguageModelCompletionProvider for FakeCompletionProvider {
         futures::future::ready(Ok(0)).boxed()
     }
 
-    fn complete(
+    fn stream_completion(
         &self,
         _request: LanguageModelRequest,
     ) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {

crates/assistant/src/completion_provider/ollama.rs ๐Ÿ”—

@@ -91,7 +91,7 @@ impl LanguageModelCompletionProvider for OllamaCompletionProvider {
         async move { Ok(token_count) }.boxed()
     }
 
-    fn complete(
+    fn stream_completion(
         &self,
         request: LanguageModelRequest,
     ) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {

crates/assistant/src/completion_provider/open_ai.rs ๐Ÿ”—

@@ -179,7 +179,7 @@ impl LanguageModelCompletionProvider for OpenAiCompletionProvider {
         count_open_ai_tokens(request, cx.background_executor())
     }
 
-    fn complete(
+    fn stream_completion(
         &self,
         request: LanguageModelRequest,
     ) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {

crates/assistant/src/context.rs ๐Ÿ”—

@@ -1,6 +1,6 @@
 use crate::{
-    slash_command::SlashCommandLine, CompletionProvider, LanguageModelRequest,
-    LanguageModelRequestMessage, MessageId, MessageStatus, Role,
+    prompt_library::PromptStore, slash_command::SlashCommandLine, CompletionProvider,
+    LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageStatus, Role,
 };
 use anyhow::{anyhow, Context as _, Result};
 use assistant_slash_command::{
@@ -10,14 +10,21 @@ use client::{proto, telemetry::Telemetry};
 use clock::ReplicaId;
 use collections::{HashMap, HashSet};
 use fs::Fs;
-use futures::{future::Shared, FutureExt, StreamExt};
+use futures::{
+    future::{self, Shared},
+    FutureExt, StreamExt,
+};
 use gpui::{AppContext, Context as _, EventEmitter, Model, ModelContext, Subscription, Task};
-use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
+use language::{
+    AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, ParseStatus, Point, ToOffset,
+};
 use open_ai::Model as OpenAiModel;
 use paths::contexts_dir;
+use project::Project;
 use serde::{Deserialize, Serialize};
 use std::{
-    cmp::Ordering,
+    cmp,
+    fmt::Debug,
     iter, mem,
     ops::Range,
     path::{Path, PathBuf},
@@ -26,7 +33,7 @@ use std::{
 };
 use telemetry_events::AssistantKind;
 use ui::SharedString;
-use util::{post_inc, TryFutureExt};
+use util::{post_inc, ResultExt, TryFutureExt};
 use uuid::Uuid;
 
 #[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
@@ -265,7 +272,7 @@ impl ContextOperation {
 pub enum ContextEvent {
     MessagesEdited,
     SummaryChanged,
-    EditSuggestionsChanged,
+    EditStepsChanged,
     StreamedCompletion,
     PendingSlashCommandsUpdated {
         removed: Vec<Range<language::Anchor>>,
@@ -326,6 +333,331 @@ struct PendingCompletion {
 #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
 pub struct SlashCommandId(clock::Lamport);
 
+#[derive(Debug)]
+pub struct EditStep {
+    pub source_range: Range<language::Anchor>,
+    pub operations: Option<EditStepOperations>,
+}
+
+#[derive(Debug)]
+pub struct EditSuggestionGroup {
+    pub context_range: Range<language::Anchor>,
+    pub suggestions: Vec<EditSuggestion>,
+}
+
+#[derive(Debug)]
+pub struct EditSuggestion {
+    pub range: Range<language::Anchor>,
+    /// If None, assume this is a suggestion to delete the range rather than transform it.
+    pub description: Option<String>,
+    pub prepend_newline: bool,
+}
+
+impl EditStep {
+    pub fn edit_suggestions(
+        &self,
+        project: &Model<Project>,
+        cx: &AppContext,
+    ) -> Task<HashMap<Model<Buffer>, Vec<EditSuggestionGroup>>> {
+        let Some(EditStepOperations::Parsed { operations, .. }) = &self.operations else {
+            return Task::ready(HashMap::default());
+        };
+
+        let suggestion_tasks: Vec<_> = operations
+            .iter()
+            .map(|operation| operation.edit_suggestion(project.clone(), cx))
+            .collect();
+
+        cx.spawn(|mut cx| async move {
+            let suggestions = future::join_all(suggestion_tasks)
+                .await
+                .into_iter()
+                .filter_map(|task| task.log_err())
+                .collect::<Vec<_>>();
+
+            let mut suggestions_by_buffer = HashMap::default();
+            for (buffer, suggestion) in suggestions {
+                suggestions_by_buffer
+                    .entry(buffer)
+                    .or_insert_with(Vec::new)
+                    .push(suggestion);
+            }
+
+            let mut suggestion_groups_by_buffer = HashMap::default();
+            for (buffer, mut suggestions) in suggestions_by_buffer {
+                let mut suggestion_groups = Vec::<EditSuggestionGroup>::new();
+                buffer
+                    .update(&mut cx, |buffer, _cx| {
+                        // Sort suggestions by their range
+                        suggestions.sort_by(|a, b| a.range.cmp(&b.range, buffer));
+
+                        // Dedup overlapping suggestions
+                        suggestions.dedup_by(|a, b| {
+                            let a_range = a.range.to_offset(buffer);
+                            let b_range = b.range.to_offset(buffer);
+                            if a_range.start <= b_range.end && b_range.start <= a_range.end {
+                                if b_range.start < a_range.start {
+                                    a.range.start = b.range.start;
+                                }
+                                if b_range.end > a_range.end {
+                                    a.range.end = b.range.end;
+                                }
+
+                                if let (Some(a_desc), Some(b_desc)) =
+                                    (a.description.as_mut(), b.description.as_mut())
+                                {
+                                    b_desc.push('\n');
+                                    b_desc.push_str(a_desc);
+                                } else if a.description.is_some() {
+                                    b.description = a.description.take();
+                                }
+
+                                true
+                            } else {
+                                false
+                            }
+                        });
+
+                        // Create context ranges for each suggestion
+                        for suggestion in suggestions {
+                            let context_range = {
+                                let suggestion_point_range = suggestion.range.to_point(buffer);
+                                let start_row = suggestion_point_range.start.row.saturating_sub(5);
+                                let end_row = cmp::min(
+                                    suggestion_point_range.end.row + 5,
+                                    buffer.max_point().row,
+                                );
+                                let start = buffer.anchor_before(Point::new(start_row, 0));
+                                let end = buffer
+                                    .anchor_after(Point::new(end_row, buffer.line_len(end_row)));
+                                start..end
+                            };
+
+                            if let Some(last_group) = suggestion_groups.last_mut() {
+                                if last_group
+                                    .context_range
+                                    .end
+                                    .cmp(&context_range.start, buffer)
+                                    .is_ge()
+                                {
+                                    // Merge with the previous group if context ranges overlap
+                                    last_group.context_range.end = context_range.end;
+                                    last_group.suggestions.push(suggestion);
+                                } else {
+                                    // Create a new group
+                                    suggestion_groups.push(EditSuggestionGroup {
+                                        context_range,
+                                        suggestions: vec![suggestion],
+                                    });
+                                }
+                            } else {
+                                // Create the first group
+                                suggestion_groups.push(EditSuggestionGroup {
+                                    context_range,
+                                    suggestions: vec![suggestion],
+                                });
+                            }
+                        }
+                    })
+                    .ok();
+                suggestion_groups_by_buffer.insert(buffer, suggestion_groups);
+            }
+
+            suggestion_groups_by_buffer
+        })
+    }
+}
+
+pub enum EditStepOperations {
+    Pending(Task<Result<()>>),
+    Parsed {
+        operations: Vec<EditOperation>,
+        raw_output: String,
+    },
+}
+
+impl Debug for EditStepOperations {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            EditStepOperations::Pending(_) => write!(f, "EditStepOperations::Pending"),
+            EditStepOperations::Parsed {
+                operations,
+                raw_output,
+            } => f
+                .debug_struct("EditStepOperations::Parsed")
+                .field("operations", operations)
+                .field("raw_output", raw_output)
+                .finish(),
+        }
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct EditOperation {
+    pub path: String,
+    pub kind: EditOperationKind,
+}
+
+impl EditOperation {
+    fn edit_suggestion(
+        &self,
+        project: Model<Project>,
+        cx: &AppContext,
+    ) -> Task<Result<(Model<language::Buffer>, EditSuggestion)>> {
+        let path = self.path.clone();
+        let kind = self.kind.clone();
+        cx.spawn(move |mut cx| async move {
+            let buffer = project
+                .update(&mut cx, |project, cx| {
+                    let project_path = project
+                        .project_path_for_full_path(Path::new(&path), cx)
+                        .with_context(|| format!("worktree not found for {:?}", path))?;
+                    anyhow::Ok(project.open_buffer(project_path, cx))
+                })??
+                .await?;
+
+            let mut parse_status = buffer.read_with(&cx, |buffer, _cx| buffer.parse_status())?;
+            while *parse_status.borrow() != ParseStatus::Idle {
+                parse_status.changed().await?;
+            }
+
+            let prepend_newline = kind.prepend_newline();
+            let suggestion_range = if let Some(symbol) = kind.symbol() {
+                let outline = buffer
+                    .update(&mut cx, |buffer, _| buffer.snapshot().outline(None))?
+                    .context("no outline for buffer")?;
+                let candidate = outline
+                    .path_candidates
+                    .iter()
+                    .find(|item| item.string == symbol)
+                    .context("symbol not found")?;
+                buffer.update(&mut cx, |buffer, _| {
+                    let outline_item = &outline.items[candidate.id];
+                    let symbol_range = outline_item.range.to_point(buffer);
+                    let body_range = outline_item
+                        .body_range
+                        .as_ref()
+                        .map(|range| range.to_point(buffer))
+                        .unwrap_or(symbol_range.clone());
+
+                    match kind {
+                        EditOperationKind::PrependChild { .. } => {
+                            let position = buffer.anchor_after(body_range.start);
+                            position..position
+                        }
+                        EditOperationKind::AppendChild { .. } => {
+                            let position = buffer.anchor_before(body_range.end);
+                            position..position
+                        }
+                        EditOperationKind::InsertSiblingBefore { .. } => {
+                            let position = buffer.anchor_before(symbol_range.start);
+                            position..position
+                        }
+                        EditOperationKind::InsertSiblingAfter { .. } => {
+                            let position = buffer.anchor_after(symbol_range.end);
+                            position..position
+                        }
+                        EditOperationKind::Update { .. } | EditOperationKind::Delete { .. } => {
+                            let start = Point::new(symbol_range.start.row, 0);
+                            let end = Point::new(
+                                symbol_range.end.row,
+                                buffer.line_len(symbol_range.end.row),
+                            );
+                            buffer.anchor_before(start)..buffer.anchor_after(end)
+                        }
+                        EditOperationKind::Create { .. } => unreachable!(),
+                    }
+                })?
+            } else {
+                match kind {
+                    EditOperationKind::PrependChild { .. } => {
+                        language::Anchor::MIN..language::Anchor::MIN
+                    }
+                    EditOperationKind::AppendChild { .. } | EditOperationKind::Create { .. } => {
+                        language::Anchor::MAX..language::Anchor::MAX
+                    }
+                    _ => unreachable!("All other operations should have a symbol"),
+                }
+            };
+
+            Ok((
+                buffer,
+                EditSuggestion {
+                    range: suggestion_range,
+                    description: kind.description().map(ToString::to_string),
+                    prepend_newline,
+                },
+            ))
+        })
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum EditOperationKind {
+    Update {
+        symbol: String,
+        description: String,
+    },
+    Create {
+        description: String,
+    },
+    InsertSiblingBefore {
+        symbol: String,
+        description: String,
+    },
+    InsertSiblingAfter {
+        symbol: String,
+        description: String,
+    },
+    PrependChild {
+        symbol: Option<String>,
+        description: String,
+    },
+    AppendChild {
+        symbol: Option<String>,
+        description: String,
+    },
+    Delete {
+        symbol: String,
+    },
+}
+
+impl EditOperationKind {
+    pub fn symbol(&self) -> Option<&str> {
+        match self {
+            Self::Update { symbol, .. } => Some(symbol),
+            Self::InsertSiblingBefore { symbol, .. } => Some(symbol),
+            Self::InsertSiblingAfter { symbol, .. } => Some(symbol),
+            Self::PrependChild { symbol, .. } => symbol.as_deref(),
+            Self::AppendChild { symbol, .. } => symbol.as_deref(),
+            Self::Delete { symbol } => Some(symbol),
+            Self::Create { .. } => None,
+        }
+    }
+
+    pub fn description(&self) -> Option<&str> {
+        match self {
+            Self::Update { description, .. } => Some(description),
+            Self::Create { description } => Some(description),
+            Self::InsertSiblingBefore { description, .. } => Some(description),
+            Self::InsertSiblingAfter { description, .. } => Some(description),
+            Self::PrependChild { description, .. } => Some(description),
+            Self::AppendChild { description, .. } => Some(description),
+            Self::Delete { .. } => None,
+        }
+    }
+
+    pub fn prepend_newline(&self) -> bool {
+        match self {
+            Self::PrependChild { .. }
+            | Self::AppendChild { .. }
+            | Self::InsertSiblingAfter { .. }
+            | Self::InsertSiblingBefore { .. } => true,
+            _ => false,
+        }
+    }
+}
+
 pub struct Context {
     id: ContextId,
     timestamp: clock::Lamport,
@@ -333,7 +665,6 @@ pub struct Context {
     pending_ops: Vec<ContextOperation>,
     operations: Vec<ContextOperation>,
     buffer: Model<Buffer>,
-    edit_suggestions: Vec<EditSuggestion>,
     pending_slash_commands: Vec<PendingSlashCommand>,
     edits_since_last_slash_command_parse: language::Subscription,
     finished_slash_commands: HashSet<SlashCommandId>,
@@ -346,12 +677,12 @@ pub struct Context {
     pending_completions: Vec<PendingCompletion>,
     token_count: Option<usize>,
     pending_token_count: Task<Option<()>>,
-    pending_edit_suggestion_parse: Option<Task<()>>,
     pending_save: Task<Result<()>>,
     path: Option<PathBuf>,
     _subscriptions: Vec<Subscription>,
     telemetry: Option<Arc<Telemetry>>,
     language_registry: Arc<LanguageRegistry>,
+    edit_steps: Vec<EditStep>,
 }
 
 impl EventEmitter<ContextEvent> for Context {}
@@ -400,7 +731,6 @@ impl Context {
             operations: Vec::new(),
             message_anchors: Default::default(),
             messages_metadata: Default::default(),
-            edit_suggestions: Vec::new(),
             pending_slash_commands: Vec::new(),
             finished_slash_commands: HashSet::default(),
             slash_command_output_sections: Vec::new(),
@@ -411,13 +741,13 @@ impl Context {
             pending_completions: Default::default(),
             token_count: None,
             pending_token_count: Task::ready(None),
-            pending_edit_suggestion_parse: None,
             _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
             pending_save: Task::ready(Ok(())),
             path: None,
             buffer,
             telemetry,
             language_registry,
+            edit_steps: Vec::new(),
         };
 
         let first_message_id = MessageId(clock::Lamport {
@@ -735,8 +1065,8 @@ impl Context {
         self.summary.as_ref()
     }
 
-    pub fn edit_suggestions(&self) -> &[EditSuggestion] {
-        &self.edit_suggestions
+    pub fn edit_steps(&self) -> &[EditStep] {
+        &self.edit_steps
     }
 
     pub fn pending_slash_commands(&self) -> &[PendingSlashCommand] {
@@ -771,8 +1101,8 @@ impl Context {
             )),
             language::Event::Edited => {
                 self.count_remaining_tokens(cx);
-                self.reparse_edit_suggestions(cx);
                 self.reparse_slash_commands(cx);
+                self.prune_invalid_edit_steps(cx);
                 cx.emit(ContextEvent::MessagesEdited);
             }
             _ => {}
@@ -880,65 +1210,245 @@ impl Context {
         }
     }
 
-    fn reparse_edit_suggestions(&mut self, cx: &mut ModelContext<Self>) {
-        self.pending_edit_suggestion_parse = Some(cx.spawn(|this, mut cx| async move {
-            cx.background_executor()
-                .timer(Duration::from_millis(200))
-                .await;
-
-            this.update(&mut cx, |this, cx| {
-                this.reparse_edit_suggestions_in_range(0..this.buffer.read(cx).len(), cx);
-            })
-            .ok();
-        }));
+    fn prune_invalid_edit_steps(&mut self, cx: &mut ModelContext<Self>) {
+        let buffer = self.buffer.read(cx);
+        let prev_len = self.edit_steps.len();
+        self.edit_steps.retain(|step| {
+            step.source_range.start.is_valid(buffer) && step.source_range.end.is_valid(buffer)
+        });
+        if self.edit_steps.len() != prev_len {
+            cx.emit(ContextEvent::EditStepsChanged);
+            cx.notify();
+        }
     }
 
-    fn reparse_edit_suggestions_in_range(
-        &mut self,
-        range: Range<usize>,
-        cx: &mut ModelContext<Self>,
-    ) {
-        self.buffer.update(cx, |buffer, _| {
-            let range_start = buffer.anchor_before(range.start);
-            let range_end = buffer.anchor_after(range.end);
-            let start_ix = self
-                .edit_suggestions
-                .binary_search_by(|probe| {
-                    probe
-                        .source_range
-                        .end
-                        .cmp(&range_start, buffer)
-                        .then(Ordering::Greater)
-                })
-                .unwrap_err();
-            let end_ix = self
-                .edit_suggestions
-                .binary_search_by(|probe| {
-                    probe
-                        .source_range
-                        .start
-                        .cmp(&range_end, buffer)
-                        .then(Ordering::Less)
-                })
-                .unwrap_err();
+    fn parse_edit_steps_in_range(&mut self, range: Range<usize>, cx: &mut ModelContext<Self>) {
+        let mut new_edit_steps = Vec::new();
 
-            let mut new_edit_suggestions = Vec::new();
+        self.buffer.update(cx, |buffer, _cx| {
             let mut message_lines = buffer.as_rope().chunks_in_range(range).lines();
-            while let Some(suggestion) = parse_next_edit_suggestion(&mut message_lines) {
-                let start_anchor = buffer.anchor_after(suggestion.outer_range.start);
-                let end_anchor = buffer.anchor_before(suggestion.outer_range.end);
-                new_edit_suggestions.push(EditSuggestion {
-                    source_range: start_anchor..end_anchor,
-                    full_path: suggestion.path,
-                });
+            let mut in_step = false;
+            let mut step_start = 0;
+            let mut line_start_offset = message_lines.offset();
+
+            while let Some(line) = message_lines.next() {
+                if let Some(step_start_index) = line.find("<step>") {
+                    if !in_step {
+                        in_step = true;
+                        step_start = line_start_offset + step_start_index;
+                    }
+                }
+
+                if let Some(step_end_index) = line.find("</step>") {
+                    if in_step {
+                        let start_anchor = buffer.anchor_after(step_start);
+                        let end_anchor = buffer
+                            .anchor_before(line_start_offset + step_end_index + "</step>".len());
+                        let source_range = start_anchor..end_anchor;
+
+                        // Check if a step with the same range already exists
+                        let existing_step_index = self.edit_steps.binary_search_by(|probe| {
+                            probe.source_range.cmp(&source_range, buffer)
+                        });
+
+                        if let Err(ix) = existing_step_index {
+                            // Step doesn't exist, so add it
+                            new_edit_steps.push((
+                                ix,
+                                EditStep {
+                                    source_range,
+                                    operations: None,
+                                },
+                            ));
+                        }
+
+                        in_step = false;
+                    }
+                }
+
+                line_start_offset = message_lines.offset();
             }
-            self.edit_suggestions
-                .splice(start_ix..end_ix, new_edit_suggestions);
         });
-        cx.emit(ContextEvent::EditSuggestionsChanged);
+
+        // Insert new steps and generate their corresponding tasks
+        for (index, mut step) in new_edit_steps.into_iter().rev() {
+            let task = self.generate_edit_step_operations(&step, cx);
+            step.operations = Some(EditStepOperations::Pending(task));
+            self.edit_steps.insert(index, step);
+        }
+
+        cx.emit(ContextEvent::EditStepsChanged);
         cx.notify();
     }
 
+    fn generate_edit_step_operations(
+        &self,
+        edit_step: &EditStep,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        let mut request = self.to_completion_request(cx);
+        let edit_step_range = edit_step.source_range.clone();
+        let step_text = self
+            .buffer
+            .read(cx)
+            .text_for_range(edit_step_range.clone())
+            .collect::<String>();
+
+        cx.spawn(|this, mut cx| async move {
+            let prompt_store = cx.update(|cx| PromptStore::global(cx))?.await?;
+
+            let mut prompt = prompt_store.operations_prompt();
+            prompt.push_str(&step_text);
+
+            request.messages.push(LanguageModelRequestMessage {
+                role: Role::User,
+                content: prompt,
+            });
+
+            let raw_output = cx
+                .update(|cx| CompletionProvider::global(cx).complete(request, cx))?
+                .await?;
+
+            let operations = Self::parse_edit_operations(&raw_output);
+            this.update(&mut cx, |this, cx| {
+                let step_index = this
+                    .edit_steps
+                    .binary_search_by(|step| {
+                        step.source_range
+                            .cmp(&edit_step_range, this.buffer.read(cx))
+                    })
+                    .map_err(|_| anyhow!("edit step not found"))?;
+                if let Some(edit_step) = this.edit_steps.get_mut(step_index) {
+                    edit_step.operations = Some(EditStepOperations::Parsed {
+                        operations,
+                        raw_output,
+                    });
+                    cx.emit(ContextEvent::EditStepsChanged);
+                }
+                anyhow::Ok(())
+            })?
+        })
+    }
+
+    fn parse_edit_operations(xml: &str) -> Vec<EditOperation> {
+        let Some(start_ix) = xml.find("<operations>") else {
+            return Vec::new();
+        };
+        let Some(end_ix) = xml[start_ix..].find("</operations>") else {
+            return Vec::new();
+        };
+        let end_ix = end_ix + start_ix + "</operations>".len();
+
+        let doc = roxmltree::Document::parse(&xml[start_ix..end_ix]).log_err();
+        doc.map_or(Vec::new(), |doc| {
+            doc.root_element()
+                .children()
+                .map(|node| {
+                    let tag_name = node.tag_name().name();
+                    let path = node
+                        .attribute("path")
+                        .with_context(|| {
+                            format!("invalid node {node:?}, missing attribute 'path'")
+                        })?
+                        .to_string();
+                    let kind = match tag_name {
+                        "update" => EditOperationKind::Update {
+                            symbol: node
+                                .attribute("symbol")
+                                .with_context(|| {
+                                    format!("invalid node {node:?}, missing attribute 'symbol'")
+                                })?
+                                .to_string(),
+                            description: node
+                                .attribute("description")
+                                .with_context(|| {
+                                    format!(
+                                        "invalid node {node:?}, missing attribute 'description'"
+                                    )
+                                })?
+                                .to_string(),
+                        },
+                        "create" => EditOperationKind::Create {
+                            description: node
+                                .attribute("description")
+                                .with_context(|| {
+                                    format!(
+                                        "invalid node {node:?}, missing attribute 'description'"
+                                    )
+                                })?
+                                .to_string(),
+                        },
+                        "insert_sibling_after" => EditOperationKind::InsertSiblingAfter {
+                            symbol: node
+                                .attribute("symbol")
+                                .with_context(|| {
+                                    format!("invalid node {node:?}, missing attribute 'symbol'")
+                                })?
+                                .to_string(),
+                            description: node
+                                .attribute("description")
+                                .with_context(|| {
+                                    format!(
+                                        "invalid node {node:?}, missing attribute 'description'"
+                                    )
+                                })?
+                                .to_string(),
+                        },
+                        "insert_sibling_before" => EditOperationKind::InsertSiblingBefore {
+                            symbol: node
+                                .attribute("symbol")
+                                .with_context(|| {
+                                    format!("invalid node {node:?}, missing attribute 'symbol'")
+                                })?
+                                .to_string(),
+                            description: node
+                                .attribute("description")
+                                .with_context(|| {
+                                    format!(
+                                        "invalid node {node:?}, missing attribute 'description'"
+                                    )
+                                })?
+                                .to_string(),
+                        },
+                        "prepend_child" => EditOperationKind::PrependChild {
+                            symbol: node.attribute("symbol").map(String::from),
+                            description: node
+                                .attribute("description")
+                                .with_context(|| {
+                                    format!(
+                                        "invalid node {node:?}, missing attribute 'description'"
+                                    )
+                                })?
+                                .to_string(),
+                        },
+                        "append_child" => EditOperationKind::AppendChild {
+                            symbol: node.attribute("symbol").map(String::from),
+                            description: node
+                                .attribute("description")
+                                .with_context(|| {
+                                    format!(
+                                        "invalid node {node:?}, missing attribute 'description'"
+                                    )
+                                })?
+                                .to_string(),
+                        },
+                        "delete" => EditOperationKind::Delete {
+                            symbol: node
+                                .attribute("symbol")
+                                .with_context(|| {
+                                    format!("invalid node {node:?}, missing attribute 'symbol'")
+                                })?
+                                .to_string(),
+                        },
+                        _ => return Err(anyhow!("invalid node {node:?}")),
+                    };
+                    anyhow::Ok(EditOperation { path, kind })
+                })
+                .filter_map(|op| op.log_err())
+                .collect()
+        })
+    }
+
     pub fn pending_command_for_position(
         &mut self,
         position: language::Anchor,
@@ -1092,159 +1602,120 @@ impl Context {
         self.count_remaining_tokens(cx);
     }
 
-    pub fn assist(
-        &mut self,
-        selected_messages: HashSet<MessageId>,
-        cx: &mut ModelContext<Self>,
-    ) -> Vec<MessageAnchor> {
-        let mut user_messages = Vec::new();
-
-        let last_message_id = if let Some(last_message_id) =
-            self.message_anchors.iter().rev().find_map(|message| {
-                message
-                    .start
-                    .is_valid(self.buffer.read(cx))
-                    .then_some(message.id)
-            }) {
-            last_message_id
-        } else {
-            return Default::default();
-        };
+    pub fn assist(&mut self, cx: &mut ModelContext<Self>) -> Option<MessageAnchor> {
+        let last_message_id = self.message_anchors.iter().rev().find_map(|message| {
+            message
+                .start
+                .is_valid(self.buffer.read(cx))
+                .then_some(message.id)
+        })?;
 
-        let mut should_assist = false;
-        for selected_message_id in selected_messages {
-            let selected_message_role =
-                if let Some(metadata) = self.messages_metadata.get(&selected_message_id) {
-                    metadata.role
-                } else {
-                    continue;
-                };
-
-            if selected_message_role == Role::Assistant {
-                if let Some(user_message) = self.insert_message_after(
-                    selected_message_id,
-                    Role::User,
-                    MessageStatus::Done,
-                    cx,
-                ) {
-                    user_messages.push(user_message);
-                }
-            } else {
-                should_assist = true;
-            }
+        if !CompletionProvider::global(cx).is_authenticated() {
+            log::info!("completion provider has no credentials");
+            return None;
         }
 
-        if should_assist {
-            if !CompletionProvider::global(cx).is_authenticated() {
-                log::info!("completion provider has no credentials");
-                return Default::default();
-            }
-
-            let request = self.to_completion_request(cx);
-            let stream = CompletionProvider::global(cx).complete(request, cx);
-            let assistant_message = self
-                .insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx)
-                .unwrap();
-
-            // Queue up the user's next reply.
-            let user_message = self
-                .insert_message_after(assistant_message.id, Role::User, MessageStatus::Done, cx)
-                .unwrap();
-            user_messages.push(user_message);
-
-            let task = cx.spawn({
-                |this, mut cx| async move {
-                    let assistant_message_id = assistant_message.id;
-                    let mut response_latency = None;
-                    let stream_completion = async {
-                        let request_start = Instant::now();
-                        let mut messages = stream.await.inner.await?;
-
-                        while let Some(message) = messages.next().await {
-                            if response_latency.is_none() {
-                                response_latency = Some(request_start.elapsed());
-                            }
-                            let text = message?;
+        let request = self.to_completion_request(cx);
+        let stream = CompletionProvider::global(cx).stream_completion(request, cx);
+        let assistant_message = self
+            .insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx)
+            .unwrap();
 
-                            this.update(&mut cx, |this, cx| {
-                                let message_ix = this
-                                    .message_anchors
-                                    .iter()
-                                    .position(|message| message.id == assistant_message_id)?;
-                                let message_range = this.buffer.update(cx, |buffer, cx| {
-                                    let message_start_offset =
-                                        this.message_anchors[message_ix].start.to_offset(buffer);
-                                    let message_old_end_offset = this.message_anchors
-                                        [message_ix + 1..]
-                                        .iter()
-                                        .find(|message| message.start.is_valid(buffer))
-                                        .map_or(buffer.len(), |message| {
-                                            message.start.to_offset(buffer).saturating_sub(1)
-                                        });
-                                    let message_new_end_offset =
-                                        message_old_end_offset + text.len();
-                                    buffer.edit(
-                                        [(message_old_end_offset..message_old_end_offset, text)],
-                                        None,
-                                        cx,
-                                    );
-                                    message_start_offset..message_new_end_offset
-                                });
-                                this.reparse_edit_suggestions_in_range(message_range, cx);
-                                cx.emit(ContextEvent::StreamedCompletion);
+        // Queue up the user's next reply.
+        let user_message = self
+            .insert_message_after(assistant_message.id, Role::User, MessageStatus::Done, cx)
+            .unwrap();
 
-                                Some(())
-                            })?;
-                            smol::future::yield_now().await;
+        let task = cx.spawn({
+            |this, mut cx| async move {
+                let assistant_message_id = assistant_message.id;
+                let mut response_latency = None;
+                let stream_completion = async {
+                    let request_start = Instant::now();
+                    let mut chunks = stream.await?;
+
+                    while let Some(chunk) = chunks.next().await {
+                        if response_latency.is_none() {
+                            response_latency = Some(request_start.elapsed());
                         }
+                        let chunk = chunk?;
 
                         this.update(&mut cx, |this, cx| {
-                            this.pending_completions
-                                .retain(|completion| completion.id != this.completion_count);
-                            this.summarize(cx);
+                            let message_ix = this
+                                .message_anchors
+                                .iter()
+                                .position(|message| message.id == assistant_message_id)?;
+                            let message_range = this.buffer.update(cx, |buffer, cx| {
+                                let message_start_offset =
+                                    this.message_anchors[message_ix].start.to_offset(buffer);
+                                let message_old_end_offset = this.message_anchors[message_ix + 1..]
+                                    .iter()
+                                    .find(|message| message.start.is_valid(buffer))
+                                    .map_or(buffer.len(), |message| {
+                                        message.start.to_offset(buffer).saturating_sub(1)
+                                    });
+                                let message_new_end_offset = message_old_end_offset + chunk.len();
+                                buffer.edit(
+                                    [(message_old_end_offset..message_old_end_offset, chunk)],
+                                    None,
+                                    cx,
+                                );
+                                message_start_offset..message_new_end_offset
+                            });
+                            this.parse_edit_steps_in_range(message_range, cx);
+                            cx.emit(ContextEvent::StreamedCompletion);
+
+                            Some(())
                         })?;
+                        smol::future::yield_now().await;
+                    }
 
-                        anyhow::Ok(())
-                    };
+                    this.update(&mut cx, |this, cx| {
+                        this.pending_completions
+                            .retain(|completion| completion.id != this.completion_count);
+                        this.summarize(cx);
+                    })?;
 
-                    let result = stream_completion.await;
+                    anyhow::Ok(())
+                };
 
-                    this.update(&mut cx, |this, cx| {
-                        let error_message = result
-                            .err()
-                            .map(|error| error.to_string().trim().to_string());
-
-                        this.update_metadata(assistant_message_id, cx, |metadata| {
-                            if let Some(error_message) = error_message.as_ref() {
-                                metadata.status =
-                                    MessageStatus::Error(SharedString::from(error_message.clone()));
-                            } else {
-                                metadata.status = MessageStatus::Done;
-                            }
-                        });
+                let result = stream_completion.await;
 
-                        if let Some(telemetry) = this.telemetry.as_ref() {
-                            let model = CompletionProvider::global(cx).model();
-                            telemetry.report_assistant_event(
-                                Some(this.id.0.clone()),
-                                AssistantKind::Panel,
-                                model.telemetry_id(),
-                                response_latency,
-                                error_message,
-                            );
+                this.update(&mut cx, |this, cx| {
+                    let error_message = result
+                        .err()
+                        .map(|error| error.to_string().trim().to_string());
+
+                    this.update_metadata(assistant_message_id, cx, |metadata| {
+                        if let Some(error_message) = error_message.as_ref() {
+                            metadata.status =
+                                MessageStatus::Error(SharedString::from(error_message.clone()));
+                        } else {
+                            metadata.status = MessageStatus::Done;
                         }
-                    })
-                    .ok();
-                }
-            });
+                    });
 
-            self.pending_completions.push(PendingCompletion {
-                id: post_inc(&mut self.completion_count),
-                _task: task,
-            });
-        }
+                    if let Some(telemetry) = this.telemetry.as_ref() {
+                        let model = CompletionProvider::global(cx).model();
+                        telemetry.report_assistant_event(
+                            Some(this.id.0.clone()),
+                            AssistantKind::Panel,
+                            model.telemetry_id(),
+                            response_latency,
+                            error_message,
+                        );
+                    }
+                })
+                .ok();
+            }
+        });
+
+        self.pending_completions.push(PendingCompletion {
+            id: post_inc(&mut self.completion_count),
+            _task: task,
+        });
 
-        user_messages
+        Some(user_message)
     }
 
     pub fn to_completion_request(&self, cx: &AppContext) -> LanguageModelRequest {

crates/assistant/src/inline_assistant.rs ๐Ÿ”—

@@ -16,7 +16,12 @@ use editor::{
     ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
 };
 use fs::Fs;
-use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
+use futures::{
+    channel::mpsc,
+    future::LocalBoxFuture,
+    stream::{self, BoxStream},
+    SinkExt, Stream, StreamExt,
+};
 use gpui::{
     point, AppContext, EventEmitter, FocusHandle, FocusableView, FontStyle, Global, HighlightStyle,
     Model, ModelContext, Subscription, Task, TextStyle, UpdateGlobal, View, ViewContext, WeakView,
@@ -28,8 +33,11 @@ use parking_lot::Mutex;
 use rope::Rope;
 use settings::{update_settings_file, Settings};
 use similar::TextDiff;
+use smol::future::FutureExt;
 use std::{
-    cmp, mem,
+    cmp,
+    future::Future,
+    mem,
     ops::{Range, RangeInclusive},
     pin::Pin,
     sync::Arc,
@@ -134,7 +142,6 @@ impl InlineAssistant {
         let prompt_buffer = cx.new_model(|cx| MultiBuffer::singleton(prompt_buffer, cx));
 
         let mut assists = Vec::new();
-        let mut assist_blocks = Vec::new();
         let mut assist_to_focus = None;
         for range in codegen_ranges {
             let assist_id = self.next_assist_id.post_inc();
@@ -142,6 +149,7 @@ impl InlineAssistant {
                 Codegen::new(
                     editor.read(cx).buffer().clone(),
                     range.clone(),
+                    None,
                     self.telemetry.clone(),
                     cx,
                 )
@@ -174,42 +182,18 @@ impl InlineAssistant {
                 }
             }
 
-            assist_blocks.push(BlockProperties {
-                style: BlockStyle::Sticky,
-                position: range.start,
-                height: prompt_editor.read(cx).height_in_lines,
-                render: build_assist_editor_renderer(&prompt_editor),
-                disposition: BlockDisposition::Above,
-            });
-            assist_blocks.push(BlockProperties {
-                style: BlockStyle::Sticky,
-                position: range.end,
-                height: 1,
-                render: Box::new(|cx| {
-                    v_flex()
-                        .h_full()
-                        .w_full()
-                        .border_t_1()
-                        .border_color(cx.theme().status().info_border)
-                        .into_any_element()
-                }),
-                disposition: BlockDisposition::Below,
-            });
-            assists.push((assist_id, prompt_editor));
-        }
+            let [prompt_block_id, end_block_id] =
+                self.insert_assist_blocks(editor, &range, &prompt_editor, cx);
 
-        let assist_block_ids = editor.update(cx, |editor, cx| {
-            editor.insert_blocks(assist_blocks, None, cx)
-        });
+            assists.push((assist_id, prompt_editor, prompt_block_id, end_block_id));
+        }
 
         let editor_assists = self
             .assists_by_editor
             .entry(editor.downgrade())
             .or_insert_with(|| EditorInlineAssists::new(&editor, cx));
         let mut assist_group = InlineAssistGroup::new();
-        for ((assist_id, prompt_editor), block_ids) in
-            assists.into_iter().zip(assist_block_ids.chunks_exact(2))
-        {
+        for (assist_id, prompt_editor, prompt_block_id, end_block_id) in assists {
             self.assists.insert(
                 assist_id,
                 InlineAssist::new(
@@ -218,8 +202,8 @@ impl InlineAssistant {
                     assistant_panel.is_some(),
                     editor,
                     &prompt_editor,
-                    block_ids[0],
-                    block_ids[1],
+                    prompt_block_id,
+                    end_block_id,
                     prompt_editor.read(cx).codegen.clone(),
                     workspace.clone(),
                     cx,
@@ -235,6 +219,128 @@ impl InlineAssistant {
         }
     }
 
+    #[allow(clippy::too_many_arguments)]
+    pub fn suggest_assist(
+        &mut self,
+        editor: &View<Editor>,
+        mut range: Range<Anchor>,
+        initial_prompt: String,
+        initial_insertion: Option<String>,
+        workspace: Option<WeakView<Workspace>>,
+        assistant_panel: Option<&View<AssistantPanel>>,
+        cx: &mut WindowContext,
+    ) -> InlineAssistId {
+        let assist_group_id = self.next_assist_group_id.post_inc();
+        let prompt_buffer = cx.new_model(|cx| Buffer::local(&initial_prompt, cx));
+        let prompt_buffer = cx.new_model(|cx| MultiBuffer::singleton(prompt_buffer, cx));
+
+        let assist_id = self.next_assist_id.post_inc();
+
+        let buffer = editor.read(cx).buffer().clone();
+        let prepend_transaction_id = initial_insertion.and_then(|initial_insertion| {
+            buffer.update(cx, |buffer, cx| {
+                buffer.start_transaction(cx);
+                buffer.edit([(range.start..range.start, initial_insertion)], None, cx);
+                buffer.end_transaction(cx)
+            })
+        });
+
+        range.start = range.start.bias_left(&buffer.read(cx).read(cx));
+        range.end = range.end.bias_right(&buffer.read(cx).read(cx));
+
+        let codegen = cx.new_model(|cx| {
+            Codegen::new(
+                editor.read(cx).buffer().clone(),
+                range.clone(),
+                prepend_transaction_id,
+                self.telemetry.clone(),
+                cx,
+            )
+        });
+
+        let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default()));
+        let prompt_editor = cx.new_view(|cx| {
+            PromptEditor::new(
+                assist_id,
+                gutter_dimensions.clone(),
+                self.prompt_history.clone(),
+                prompt_buffer.clone(),
+                codegen.clone(),
+                editor,
+                assistant_panel,
+                workspace.clone(),
+                self.fs.clone(),
+                cx,
+            )
+        });
+
+        let [prompt_block_id, end_block_id] =
+            self.insert_assist_blocks(editor, &range, &prompt_editor, cx);
+
+        let editor_assists = self
+            .assists_by_editor
+            .entry(editor.downgrade())
+            .or_insert_with(|| EditorInlineAssists::new(&editor, cx));
+
+        let mut assist_group = InlineAssistGroup::new();
+        self.assists.insert(
+            assist_id,
+            InlineAssist::new(
+                assist_id,
+                assist_group_id,
+                assistant_panel.is_some(),
+                editor,
+                &prompt_editor,
+                prompt_block_id,
+                end_block_id,
+                prompt_editor.read(cx).codegen.clone(),
+                workspace.clone(),
+                cx,
+            ),
+        );
+        assist_group.assist_ids.push(assist_id);
+        editor_assists.assist_ids.push(assist_id);
+        self.assist_groups.insert(assist_group_id, assist_group);
+        assist_id
+    }
+
+    fn insert_assist_blocks(
+        &self,
+        editor: &View<Editor>,
+        range: &Range<Anchor>,
+        prompt_editor: &View<PromptEditor>,
+        cx: &mut WindowContext,
+    ) -> [BlockId; 2] {
+        let assist_blocks = vec![
+            BlockProperties {
+                style: BlockStyle::Sticky,
+                position: range.start,
+                height: prompt_editor.read(cx).height_in_lines,
+                render: build_assist_editor_renderer(prompt_editor),
+                disposition: BlockDisposition::Above,
+            },
+            BlockProperties {
+                style: BlockStyle::Sticky,
+                position: range.end,
+                height: 1,
+                render: Box::new(|cx| {
+                    v_flex()
+                        .h_full()
+                        .w_full()
+                        .border_t_1()
+                        .border_color(cx.theme().status().info_border)
+                        .into_any_element()
+                }),
+                disposition: BlockDisposition::Below,
+            },
+        ];
+
+        editor.update(cx, |editor, cx| {
+            let block_ids = editor.insert_blocks(assist_blocks, None, cx);
+            [block_ids[0], block_ids[1]]
+        })
+    }
+
     fn handle_prompt_editor_focus_in(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
         let assist = &self.assists[&assist_id];
         let Some(decorations) = assist.decorations.as_ref() else {
@@ -379,6 +485,14 @@ impl InlineAssistant {
         cx.propagate();
     }
 
+    fn handle_editor_release(&mut self, editor: WeakView<Editor>, cx: &mut WindowContext) {
+        if let Some(editor_assists) = self.assists_by_editor.get_mut(&editor) {
+            for assist_id in editor_assists.assist_ids.clone() {
+                self.finish_assist(assist_id, true, cx);
+            }
+        }
+    }
+
     fn handle_editor_change(&mut self, editor: View<Editor>, cx: &mut WindowContext) {
         let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) else {
             return;
@@ -698,7 +812,7 @@ impl InlineAssistant {
         assist_group.assist_ids.clone()
     }
 
-    fn start_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
+    pub fn start_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
         let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
             assist
         } else {
@@ -727,16 +841,26 @@ impl InlineAssistant {
             self.prompt_history.pop_front();
         }
 
-        assist.codegen.update(cx, |codegen, cx| codegen.undo(cx));
         let codegen = assist.codegen.clone();
-        let request = self.request_for_inline_assist(assist_id, cx);
-
-        cx.spawn(|mut cx| async move {
-            let request = request.await?;
-            codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx))?;
-            anyhow::Ok(())
-        })
-        .detach_and_log_err(cx);
+        let telemetry_id = CompletionProvider::global(cx).model().telemetry_id();
+        let chunks: LocalBoxFuture<Result<BoxStream<Result<String>>>> =
+            if user_prompt.trim().to_lowercase() == "delete" {
+                async { Ok(stream::empty().boxed()) }.boxed_local()
+            } else {
+                let request = self.request_for_inline_assist(assist_id, cx);
+                let mut cx = cx.to_async();
+                async move {
+                    let request = request.await?;
+                    let chunks = cx
+                        .update(|cx| CompletionProvider::global(cx).stream_completion(request, cx))?
+                        .await?;
+                    Ok(chunks.boxed())
+                }
+                .boxed_local()
+            };
+        codegen.update(cx, |codegen, cx| {
+            codegen.start(telemetry_id, chunks, cx);
+        });
     }
 
     fn request_for_inline_assist(
@@ -855,7 +979,7 @@ impl InlineAssistant {
         })
     }
 
-    fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
+    pub fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
         let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
             assist
         } else {
@@ -1074,6 +1198,14 @@ impl EditorInlineAssists {
                 }
             }),
             _subscriptions: vec![
+                cx.observe_release(editor, {
+                    let editor = editor.downgrade();
+                    |_, cx| {
+                        InlineAssistant::update_global(cx, |this, cx| {
+                            this.handle_editor_release(editor, cx);
+                        })
+                    }
+                }),
                 cx.observe(editor, move |editor, cx| {
                     InlineAssistant::update_global(cx, |this, cx| {
                         this.handle_editor_change(editor, cx)
@@ -1138,7 +1270,7 @@ fn build_assist_editor_renderer(editor: &View<PromptEditor>) -> RenderBlock {
 }
 
 #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
-struct InlineAssistId(usize);
+pub struct InlineAssistId(usize);
 
 impl InlineAssistId {
     fn post_inc(&mut self) -> InlineAssistId {
@@ -1882,7 +2014,8 @@ pub struct Codegen {
     range: Range<Anchor>,
     edit_position: Anchor,
     last_equal_ranges: Vec<Range<Anchor>>,
-    transaction_id: Option<TransactionId>,
+    prepend_transaction_id: Option<TransactionId>,
+    generation_transaction_id: Option<TransactionId>,
     status: CodegenStatus,
     generation: Task<()>,
     diff: Diff,
@@ -1911,6 +2044,7 @@ impl Codegen {
     pub fn new(
         buffer: Model<MultiBuffer>,
         range: Range<Anchor>,
+        prepend_transaction_id: Option<TransactionId>,
         telemetry: Option<Arc<Telemetry>>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
@@ -1943,7 +2077,8 @@ impl Codegen {
             range,
             snapshot,
             last_equal_ranges: Default::default(),
-            transaction_id: Default::default(),
+            prepend_transaction_id,
+            generation_transaction_id: None,
             status: CodegenStatus::Idle,
             generation: Task::ready(()),
             diff: Diff::default(),
@@ -1959,8 +2094,13 @@ impl Codegen {
         cx: &mut ModelContext<Self>,
     ) {
         if let multi_buffer::Event::TransactionUndone { transaction_id } = event {
-            if self.transaction_id == Some(*transaction_id) {
-                self.transaction_id = None;
+            if self.generation_transaction_id == Some(*transaction_id) {
+                self.generation_transaction_id = None;
+                self.generation = Task::ready(());
+                cx.emit(CodegenEvent::Undone);
+            } else if self.prepend_transaction_id == Some(*transaction_id) {
+                self.prepend_transaction_id = None;
+                self.generation_transaction_id = None;
                 self.generation = Task::ready(());
                 cx.emit(CodegenEvent::Undone);
             }
@@ -1971,7 +2111,12 @@ impl Codegen {
         &self.last_equal_ranges
     }
 
-    pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
+    pub fn start(
+        &mut self,
+        telemetry_id: String,
+        stream: impl 'static + Future<Output = Result<BoxStream<'static, Result<String>>>>,
+        cx: &mut ModelContext<Self>,
+    ) {
         let range = self.range.clone();
         let snapshot = self.snapshot.clone();
         let selected_text = snapshot
@@ -1985,15 +2130,17 @@ impl Codegen {
             .next()
             .unwrap_or_else(|| snapshot.indent_size_for_line(MultiBufferRow(selection_start.row)));
 
-        let model_telemetry_id = prompt.model.telemetry_id();
-        let response = CompletionProvider::global(cx).complete(prompt, cx);
         let telemetry = self.telemetry.clone();
         self.edit_position = range.start;
         self.diff = Diff::default();
         self.status = CodegenStatus::Pending;
+        if let Some(transaction_id) = self.generation_transaction_id.take() {
+            self.buffer
+                .update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
+        }
         self.generation = cx.spawn(|this, mut cx| {
             async move {
-                let response = response.await;
+                let chunks = stream.await;
                 let generate = async {
                     let mut edit_start = range.start.to_offset(&snapshot);
 
@@ -2003,7 +2150,7 @@ impl Codegen {
                             let mut response_latency = None;
                             let request_start = Instant::now();
                             let diff = async {
-                                let chunks = StripInvalidSpans::new(response.inner.await?);
+                                let chunks = StripInvalidSpans::new(chunks?);
                                 futures::pin_mut!(chunks);
                                 let mut diff = StreamingDiff::new(selected_text.to_string());
 
@@ -2086,7 +2233,7 @@ impl Codegen {
                                 telemetry.report_assistant_event(
                                     None,
                                     telemetry_events::AssistantKind::Inline,
-                                    model_telemetry_id,
+                                    telemetry_id,
                                     response_latency,
                                     error_message,
                                 );
@@ -2136,7 +2283,7 @@ impl Codegen {
                             });
 
                             if let Some(transaction) = transaction {
-                                if let Some(first_transaction) = this.transaction_id {
+                                if let Some(first_transaction) = this.generation_transaction_id {
                                     // Group all assistant edits into the first transaction.
                                     this.buffer.update(cx, |buffer, cx| {
                                         buffer.merge_transactions(
@@ -2146,7 +2293,7 @@ impl Codegen {
                                         )
                                     });
                                 } else {
-                                    this.transaction_id = Some(transaction);
+                                    this.generation_transaction_id = Some(transaction);
                                     this.buffer.update(cx, |buffer, cx| {
                                         buffer.finalize_last_transaction(cx)
                                     });
@@ -2189,7 +2336,12 @@ impl Codegen {
     }
 
     pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
-        if let Some(transaction_id) = self.transaction_id.take() {
+        if let Some(transaction_id) = self.prepend_transaction_id.take() {
+            self.buffer
+                .update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
+        }
+
+        if let Some(transaction_id) = self.generation_transaction_id.take() {
             self.buffer
                 .update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
         }
@@ -2451,11 +2603,8 @@ fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
 
 #[cfg(test)]
 mod tests {
-    use std::sync::Arc;
-
-    use crate::FakeCompletionProvider;
-
     use super::*;
+    use crate::FakeCompletionProvider;
     use futures::stream::{self};
     use gpui::{Context, TestAppContext};
     use indoc::indoc;
@@ -2466,6 +2615,7 @@ mod tests {
     use rand::prelude::*;
     use serde::Serialize;
     use settings::SettingsStore;
+    use std::{future, sync::Arc};
 
     #[derive(Serialize)]
     pub struct DummyCompletionRequest {
@@ -2475,7 +2625,7 @@ mod tests {
     #[gpui::test(iterations = 10)]
     async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
         cx.set_global(cx.update(SettingsStore::test));
-        let provider = cx.update(|cx| FakeCompletionProvider::setup_test(cx));
+        cx.update(|cx| FakeCompletionProvider::setup_test(cx));
         cx.update(language_settings::init);
 
         let text = indoc! {"
@@ -2493,14 +2643,17 @@ mod tests {
             let snapshot = buffer.snapshot(cx);
             snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
         });
-        let codegen = cx.new_model(|cx| Codegen::new(buffer.clone(), range, None, cx));
+        let codegen = cx.new_model(|cx| Codegen::new(buffer.clone(), range, None, None, cx));
 
+        let (chunks_tx, chunks_rx) = mpsc::unbounded();
         codegen.update(cx, |codegen, cx| {
-            codegen.start(LanguageModelRequest::default(), cx)
+            codegen.start(
+                String::new(),
+                future::ready(Ok(chunks_rx.map(|chunk| Ok(chunk)).boxed())),
+                cx,
+            )
         });
 
-        cx.background_executor.run_until_parked();
-
         let mut new_text = concat!(
             "       let mut x = 0;\n",
             "       while x < 10 {\n",
@@ -2511,11 +2664,11 @@ mod tests {
             let max_len = cmp::min(new_text.len(), 10);
             let len = rng.gen_range(1..=max_len);
             let (chunk, suffix) = new_text.split_at(len);
-            provider.send_completion(&LanguageModelRequest::default(), chunk.into());
+            chunks_tx.unbounded_send(chunk.to_string()).unwrap();
             new_text = suffix;
             cx.background_executor.run_until_parked();
         }
-        provider.finish_completion(&LanguageModelRequest::default());
+        drop(chunks_tx);
         cx.background_executor.run_until_parked();
 
         assert_eq!(
@@ -2536,7 +2689,6 @@ mod tests {
         cx: &mut TestAppContext,
         mut rng: StdRng,
     ) {
-        let provider = cx.update(|cx| FakeCompletionProvider::setup_test(cx));
         cx.set_global(cx.update(SettingsStore::test));
         cx.update(language_settings::init);
 
@@ -2552,10 +2704,16 @@ mod tests {
             let snapshot = buffer.snapshot(cx);
             snapshot.anchor_before(Point::new(1, 6))..snapshot.anchor_after(Point::new(1, 6))
         });
-        let codegen = cx.new_model(|cx| Codegen::new(buffer.clone(), range, None, cx));
+        let codegen = cx.new_model(|cx| Codegen::new(buffer.clone(), range, None, None, cx));
 
-        let request = LanguageModelRequest::default();
-        codegen.update(cx, |codegen, cx| codegen.start(request, cx));
+        let (chunks_tx, chunks_rx) = mpsc::unbounded();
+        codegen.update(cx, |codegen, cx| {
+            codegen.start(
+                String::new(),
+                future::ready(Ok(chunks_rx.map(|chunk| Ok(chunk)).boxed())),
+                cx,
+            )
+        });
 
         cx.background_executor.run_until_parked();
 
@@ -2569,11 +2727,11 @@ mod tests {
             let max_len = cmp::min(new_text.len(), 10);
             let len = rng.gen_range(1..=max_len);
             let (chunk, suffix) = new_text.split_at(len);
-            provider.send_completion(&LanguageModelRequest::default(), chunk.into());
+            chunks_tx.unbounded_send(chunk.to_string()).unwrap();
             new_text = suffix;
             cx.background_executor.run_until_parked();
         }
-        provider.finish_completion(&LanguageModelRequest::default());
+        drop(chunks_tx);
         cx.background_executor.run_until_parked();
 
         assert_eq!(
@@ -2594,7 +2752,7 @@ mod tests {
         cx: &mut TestAppContext,
         mut rng: StdRng,
     ) {
-        let provider = cx.update(|cx| FakeCompletionProvider::setup_test(cx));
+        cx.update(|cx| FakeCompletionProvider::setup_test(cx));
         cx.set_global(cx.update(SettingsStore::test));
         cx.update(language_settings::init);
 
@@ -2610,10 +2768,16 @@ mod tests {
             let snapshot = buffer.snapshot(cx);
             snapshot.anchor_before(Point::new(1, 2))..snapshot.anchor_after(Point::new(1, 2))
         });
-        let codegen = cx.new_model(|cx| Codegen::new(buffer.clone(), range, None, cx));
+        let codegen = cx.new_model(|cx| Codegen::new(buffer.clone(), range, None, None, cx));
 
-        let request = LanguageModelRequest::default();
-        codegen.update(cx, |codegen, cx| codegen.start(request, cx));
+        let (chunks_tx, chunks_rx) = mpsc::unbounded();
+        codegen.update(cx, |codegen, cx| {
+            codegen.start(
+                String::new(),
+                future::ready(Ok(chunks_rx.map(|chunk| Ok(chunk)).boxed())),
+                cx,
+            )
+        });
 
         cx.background_executor.run_until_parked();
 
@@ -2627,11 +2791,11 @@ mod tests {
             let max_len = cmp::min(new_text.len(), 10);
             let len = rng.gen_range(1..=max_len);
             let (chunk, suffix) = new_text.split_at(len);
-            provider.send_completion(&LanguageModelRequest::default(), chunk.into());
+            chunks_tx.unbounded_send(chunk.to_string()).unwrap();
             new_text = suffix;
             cx.background_executor.run_until_parked();
         }
-        provider.finish_completion(&LanguageModelRequest::default());
+        drop(chunks_tx);
         cx.background_executor.run_until_parked();
 
         assert_eq!(

crates/assistant/src/prompt_library.rs ๐Ÿ”—

@@ -3,6 +3,7 @@ use crate::{
     InlineAssist, InlineAssistant, LanguageModelRequest, LanguageModelRequestMessage, Role,
 };
 use anyhow::{anyhow, Result};
+use assets::Assets;
 use chrono::{DateTime, Utc};
 use collections::{HashMap, HashSet};
 use editor::{actions::Tab, CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle};
@@ -12,8 +13,8 @@ use futures::{
 };
 use fuzzy::StringMatchCandidate;
 use gpui::{
-    actions, point, size, transparent_black, AppContext, BackgroundExecutor, Bounds, EventEmitter,
-    Global, HighlightStyle, PromptLevel, ReadGlobal, Subscription, Task, TextStyle,
+    actions, point, size, transparent_black, AppContext, AssetSource, BackgroundExecutor, Bounds,
+    EventEmitter, Global, HighlightStyle, PromptLevel, ReadGlobal, Subscription, Task, TextStyle,
     TitlebarOptions, UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
 };
 use heed::{types::SerdeBincode, Database, RoTxn};
@@ -1296,6 +1297,17 @@ impl PromptStore {
     fn first(&self) -> Option<PromptMetadata> {
         self.metadata_cache.read().metadata.first().cloned()
     }
+
+    pub fn operations_prompt(&self) -> String {
+        String::from_utf8(
+            Assets
+                .load("prompts/operations.md")
+                .unwrap()
+                .unwrap()
+                .to_vec(),
+        )
+        .unwrap()
+    }
 }
 
 /// Wraps a shared future to a prompt store so it can be assigned as a context global.

crates/assistant/src/search.rs ๐Ÿ”—

@@ -1,171 +0,0 @@
-use language::Rope;
-use std::ops::Range;
-
-/// Search the given buffer for the given substring, ignoring any differences
-/// in line indentation between the query and the buffer.
-///
-/// Returns a vector of ranges of byte offsets in the buffer corresponding
-/// to the entire lines of the buffer.
-pub fn fuzzy_search_lines(haystack: &Rope, needle: &str) -> Option<Range<usize>> {
-    const SIMILARITY_THRESHOLD: f64 = 0.8;
-
-    let mut best_match: Option<(Range<usize>, f64)> = None; // (range, score)
-    let mut haystack_lines = haystack.chunks().lines();
-    let mut haystack_line_start = 0;
-    while let Some(mut haystack_line) = haystack_lines.next() {
-        let next_haystack_line_start = haystack_line_start + haystack_line.len() + 1;
-        let mut advanced_to_next_haystack_line = false;
-
-        let mut matched = true;
-        let match_start = haystack_line_start;
-        let mut match_end = next_haystack_line_start;
-        let mut match_score = 0.0;
-        let mut needle_lines = needle.lines().peekable();
-        while let Some(needle_line) = needle_lines.next() {
-            let similarity = line_similarity(haystack_line, needle_line);
-            if similarity >= SIMILARITY_THRESHOLD {
-                match_end = haystack_lines.offset();
-                match_score += similarity;
-
-                if needle_lines.peek().is_some() {
-                    if let Some(next_haystack_line) = haystack_lines.next() {
-                        advanced_to_next_haystack_line = true;
-                        haystack_line = next_haystack_line;
-                    } else {
-                        matched = false;
-                        break;
-                    }
-                } else {
-                    break;
-                }
-            } else {
-                matched = false;
-                break;
-            }
-        }
-
-        if matched
-            && best_match
-                .as_ref()
-                .map(|(_, best_score)| match_score > *best_score)
-                .unwrap_or(true)
-        {
-            best_match = Some((match_start..match_end, match_score));
-        }
-
-        if advanced_to_next_haystack_line {
-            haystack_lines.seek(next_haystack_line_start);
-        }
-        haystack_line_start = next_haystack_line_start;
-    }
-
-    best_match.map(|(range, _)| range)
-}
-
-/// Calculates the similarity between two lines, ignoring leading and trailing whitespace,
-/// using the Jaro-Winkler distance.
-///
-/// Returns a value between 0.0 and 1.0, where 1.0 indicates an exact match.
-fn line_similarity(line1: &str, line2: &str) -> f64 {
-    strsim::jaro_winkler(line1.trim(), line2.trim())
-}
-
-#[cfg(test)]
-mod test {
-    use super::*;
-    use gpui::{AppContext, Context as _};
-    use language::Buffer;
-    use unindent::Unindent as _;
-    use util::test::marked_text_ranges;
-
-    #[gpui::test]
-    fn test_fuzzy_search_lines(cx: &mut AppContext) {
-        let (text, expected_ranges) = marked_text_ranges(
-            &r#"
-            fn main() {
-                if a() {
-                    assert_eq!(
-                        1 + 2,
-                        does_not_match,
-                    );
-                }
-
-                println!("hi");
-
-                assert_eq!(
-                    1 + 2,
-                    3,
-                ); // this last line does not match
-
-            ยซ    assert_eq!(
-                    1 + 2,
-                    3,
-                );
-            ยป
-
-            ยซ    assert_eq!(
-                    "something",
-                    "else",
-                );
-            ยป
-            }
-            "#
-            .unindent(),
-            false,
-        );
-
-        let buffer = cx.new_model(|cx| Buffer::local(&text, cx));
-        let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
-
-        let actual_range = fuzzy_search_lines(
-            snapshot.as_rope(),
-            &"
-            assert_eq!(
-                1 + 2,
-                3,
-            );
-            "
-            .unindent(),
-        )
-        .unwrap();
-        assert_eq!(actual_range, expected_ranges[0]);
-
-        let actual_range = fuzzy_search_lines(
-            snapshot.as_rope(),
-            &"
-            assert_eq!(
-                1 + 2,
-                3,
-            );
-            "
-            .unindent(),
-        )
-        .unwrap();
-        assert_eq!(actual_range, expected_ranges[0]);
-
-        let actual_range = fuzzy_search_lines(
-            snapshot.as_rope(),
-            &"
-            asst_eq!(
-                \"something\",
-                \"els\"
-            )
-            "
-            .unindent(),
-        )
-        .unwrap();
-        assert_eq!(actual_range, expected_ranges[1]);
-
-        let actual_range = fuzzy_search_lines(
-            snapshot.as_rope(),
-            &"
-            assert_eq!(
-                2 + 1,
-                3,
-            );
-            "
-            .unindent(),
-        );
-        assert_eq!(actual_range, None);
-    }
-}

crates/assistant/src/terminal_inline_assistant.rs ๐Ÿ”—

@@ -1026,7 +1026,7 @@ impl Codegen {
 
         let telemetry = self.telemetry.clone();
         let model_telemetry_id = prompt.model.telemetry_id();
-        let response = CompletionProvider::global(cx).complete(prompt, cx);
+        let response = CompletionProvider::global(cx).stream_completion(prompt, cx);
 
         self.generation = cx.spawn(|this, mut cx| async move {
             let response = response.await;
@@ -1037,8 +1037,8 @@ impl Codegen {
                     let mut response_latency = None;
                     let request_start = Instant::now();
                     let task = async {
-                        let mut response = response.inner.await?;
-                        while let Some(chunk) = response.next().await {
+                        let mut chunks = response?;
+                        while let Some(chunk) = chunks.next().await {
                             if response_latency.is_none() {
                                 response_latency = Some(request_start.elapsed());
                             }

crates/auto_update/src/auto_update.rs ๐Ÿ”—

@@ -288,7 +288,12 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
                                 Some(tab_description),
                                 cx,
                             );
-                            workspace.add_item_to_active_pane(Box::new(view.clone()), None, cx);
+                            workspace.add_item_to_active_pane(
+                                Box::new(view.clone()),
+                                None,
+                                true,
+                                cx,
+                            );
                             cx.notify();
                         })
                         .log_err();

crates/collab/src/tests/following_tests.rs ๐Ÿ”—

@@ -266,7 +266,7 @@ async fn test_basic_following(
 
     // When client A activates a different editor, client B does so as well.
     workspace_a.update(cx_a, |workspace, cx| {
-        workspace.activate_item(&editor_a1, cx)
+        workspace.activate_item(&editor_a1, true, true, cx)
     });
     executor.run_until_parked();
     workspace_b.update(cx_b, |workspace, cx| {
@@ -311,7 +311,7 @@ async fn test_basic_following(
         let editor = cx.new_view(|cx| {
             Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), true, cx)
         });
-        workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
+        workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, cx);
         editor
     });
     executor.run_until_parked();
@@ -401,7 +401,7 @@ async fn test_basic_following(
         workspace.unfollow(peer_id_a, cx).unwrap()
     });
     workspace_a.update(cx_a, |workspace, cx| {
-        workspace.activate_item(&editor_a2, cx)
+        workspace.activate_item(&editor_a2, true, true, cx)
     });
     executor.run_until_parked();
     assert_eq!(
@@ -466,7 +466,7 @@ async fn test_basic_following(
 
     // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
     workspace_b.update(cx_b, |workspace, cx| {
-        workspace.activate_item(&multibuffer_editor_b, cx)
+        workspace.activate_item(&multibuffer_editor_b, true, true, cx)
     });
     executor.run_until_parked();
     workspace_a.update(cx_a, |workspace, cx| {

crates/command_palette/src/command_palette.rs ๐Ÿ”—

@@ -477,7 +477,7 @@ mod tests {
         });
 
         workspace.update(cx, |workspace, cx| {
-            workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
+            workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, cx);
             editor.update(cx, |editor, cx| editor.focus(cx))
         });
 

crates/diagnostics/src/diagnostics.rs ๐Ÿ”—

@@ -237,13 +237,13 @@ impl ProjectDiagnosticsEditor {
 
     fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
         if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
-            workspace.activate_item(&existing, cx);
+            workspace.activate_item(&existing, true, true, cx);
         } else {
             let workspace_handle = cx.view().downgrade();
             let diagnostics = cx.new_view(|cx| {
                 ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
             });
-            workspace.add_item_to_active_pane(Box::new(diagnostics), None, cx);
+            workspace.add_item_to_active_pane(Box::new(diagnostics), None, true, cx);
         }
     }
 

crates/diagnostics/src/grouped_diagnostics.rs ๐Ÿ”—

@@ -250,13 +250,13 @@ impl GroupedDiagnosticsEditor {
 
     fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
         if let Some(existing) = workspace.item_of_type::<GroupedDiagnosticsEditor>(cx) {
-            workspace.activate_item(&existing, cx);
+            workspace.activate_item(&existing, true, true, cx);
         } else {
             let workspace_handle = cx.view().downgrade();
             let diagnostics = cx.new_view(|cx| {
                 GroupedDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
             });
-            workspace.add_item_to_active_pane(Box::new(diagnostics), None, cx);
+            workspace.add_item_to_active_pane(Box::new(diagnostics), None, true, cx);
         }
     }
 

crates/editor/src/editor.rs ๐Ÿ”—

@@ -1992,28 +1992,35 @@ impl Editor {
         _: &workspace::NewFile,
         cx: &mut ViewContext<Workspace>,
     ) {
+        Self::new_in_workspace(workspace, cx).detach_and_prompt_err(
+            "Failed to create buffer",
+            cx,
+            |e, _| match e.error_code() {
+                ErrorCode::RemoteUpgradeRequired => Some(format!(
+                "The remote instance of Zed does not support this yet. It must be upgraded to {}",
+                e.error_tag("required").unwrap_or("the latest version")
+            )),
+                _ => None,
+            },
+        );
+    }
+
+    pub fn new_in_workspace(
+        workspace: &mut Workspace,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Task<Result<View<Editor>>> {
         let project = workspace.project().clone();
         let create = project.update(cx, |project, cx| project.create_buffer(cx));
 
         cx.spawn(|workspace, mut cx| async move {
             let buffer = create.await?;
             workspace.update(&mut cx, |workspace, cx| {
-                workspace.add_item_to_active_pane(
-                    Box::new(
-                        cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
-                    ),
-                    None,
-                    cx,
-                )
+                let editor =
+                    cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx));
+                workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, cx);
+                editor
             })
         })
-        .detach_and_prompt_err("Failed to create buffer", cx, |e, _| match e.error_code() {
-            ErrorCode::RemoteUpgradeRequired => Some(format!(
-                "The remote instance of Zed does not support this yet. It must be upgraded to {}",
-                e.error_tag("required").unwrap_or("the latest version")
-            )),
-            _ => None,
-        });
     }
 
     pub fn new_file_in_direction(
@@ -4658,7 +4665,7 @@ impl Editor {
             let project = workspace.project().clone();
             let editor =
                 cx.new_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), true, cx));
-            workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
+            workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, cx);
             editor.update(cx, |editor, cx| {
                 editor.highlight_background::<Self>(
                     &ranges_to_highlight,
@@ -9093,7 +9100,13 @@ impl Editor {
                                             workspace.active_pane().clone()
                                         };
 
-                                        workspace.open_project_item(pane, target.buffer.clone(), cx)
+                                        workspace.open_project_item(
+                                            pane,
+                                            target.buffer.clone(),
+                                            true,
+                                            true,
+                                            cx,
+                                        )
                                     });
                                 target_editor.update(cx, |target_editor, cx| {
                                     // When selecting a definition in a different buffer, disable the nav history
@@ -9391,7 +9404,7 @@ impl Editor {
                     None
                 }
             });
-            workspace.add_item_to_active_pane(item.clone(), destination_index, cx);
+            workspace.add_item_to_active_pane(item.clone(), destination_index, true, cx);
         }
         workspace.active_pane().update(cx, |pane, cx| {
             pane.set_preview_item_id(Some(item_id), cx);
@@ -11342,7 +11355,8 @@ impl Editor {
                 };
 
                 for (buffer, ranges) in new_selections_by_buffer {
-                    let editor = workspace.open_project_item::<Self>(pane.clone(), buffer, cx);
+                    let editor =
+                        workspace.open_project_item::<Self>(pane.clone(), buffer, true, true, cx);
                     editor.update(cx, |editor, cx| {
                         editor.change_selections(Some(Autoscroll::newest()), cx, |s| {
                             s.select_ranges(ranges);

crates/editor/src/editor_tests.rs ๐Ÿ”—

@@ -10445,7 +10445,12 @@ async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
                 workspace.active_item(cx).is_none(),
                 "active item should be None before the first item is added"
             );
-            workspace.add_item_to_active_pane(Box::new(multi_buffer_editor.clone()), None, cx);
+            workspace.add_item_to_active_pane(
+                Box::new(multi_buffer_editor.clone()),
+                None,
+                true,
+                cx,
+            );
             let active_item = workspace
                 .active_item(cx)
                 .expect("should have an active item after adding the multi buffer");

crates/extensions_ui/src/extensions_ui.rs ๐Ÿ”—

@@ -48,10 +48,10 @@ pub fn init(cx: &mut AppContext) {
                     .find_map(|item| item.downcast::<ExtensionsPage>());
 
                 if let Some(existing) = existing {
-                    workspace.activate_item(&existing, cx);
+                    workspace.activate_item(&existing, true, true, cx);
                 } else {
                     let extensions_page = ExtensionsPage::new(workspace, cx);
-                    workspace.add_item_to_active_pane(Box::new(extensions_page), None, cx)
+                    workspace.add_item_to_active_pane(Box::new(extensions_page), None, true, cx)
                 }
             })
             .register_action(move |workspace, _: &InstallDevExtension, cx| {

crates/gpui/src/window.rs ๐Ÿ”—

@@ -1161,6 +1161,29 @@ impl<'a> WindowContext<'a> {
         )
     }
 
+    /// Register a callback to be invoked when the given Model or View is released.
+    pub fn observe_release<E, T>(
+        &mut self,
+        entity: &E,
+        mut on_release: impl FnOnce(&mut T, &mut WindowContext) + 'static,
+    ) -> Subscription
+    where
+        E: Entity<T>,
+        T: 'static,
+    {
+        let entity_id = entity.entity_id();
+        let window_handle = self.window.handle;
+        let (subscription, activate) = self.app.release_listeners.insert(
+            entity_id,
+            Box::new(move |entity, cx| {
+                let entity = entity.downcast_mut().expect("invalid entity type");
+                let _ = window_handle.update(cx, |_, cx| on_release(entity, cx));
+            }),
+        );
+        activate();
+        subscription
+    }
+
     /// Creates an [`AsyncWindowContext`], which has a static lifetime and can be held across
     /// await points in async code.
     pub fn to_async(&self) -> AsyncWindowContext {

crates/language/Cargo.toml ๐Ÿ”—

@@ -27,6 +27,7 @@ test-support = [
 [dependencies]
 anyhow.workspace = true
 async-trait.workspace = true
+async-watch.workspace = true
 clock.workspace = true
 collections.workspace = true
 futures.workspace = true

crates/language/src/buffer.rs ๐Ÿ”—

@@ -17,6 +17,7 @@ use crate::{
     LanguageScope, Outline, RunnableCapture, RunnableTag,
 };
 use anyhow::{anyhow, Context, Result};
+use async_watch as watch;
 pub use clock::ReplicaId;
 use futures::channel::oneshot;
 use gpui::{
@@ -32,7 +33,7 @@ use smol::future::yield_now;
 use std::{
     any::Any,
     cell::Cell,
-    cmp::{self, Ordering},
+    cmp::{self, Ordering, Reverse},
     collections::BTreeMap,
     ffi::OsStr,
     fmt,
@@ -104,6 +105,7 @@ pub struct Buffer {
     sync_parse_timeout: Duration,
     syntax_map: Mutex<SyntaxMap>,
     parsing_in_background: bool,
+    parse_status: (watch::Sender<ParseStatus>, watch::Receiver<ParseStatus>),
     non_text_state_update_count: usize,
     diagnostics: SmallVec<[(LanguageServerId, DiagnosticSet); 2]>,
     remote_selections: TreeMap<ReplicaId, SelectionSet>,
@@ -119,6 +121,12 @@ pub struct Buffer {
     has_unsaved_edits: Cell<(clock::Global, bool)>,
 }
 
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum ParseStatus {
+    Idle,
+    Parsing,
+}
+
 /// An immutable, cheaply cloneable representation of a fixed
 /// state of a buffer.
 pub struct BufferSnapshot {
@@ -710,6 +718,7 @@ impl Buffer {
             parsing_in_background: false,
             non_text_state_update_count: 0,
             sync_parse_timeout: Duration::from_millis(1),
+            parse_status: async_watch::channel(ParseStatus::Idle),
             autoindent_requests: Default::default(),
             pending_autoindent: Default::default(),
             language: None,
@@ -1059,6 +1068,7 @@ impl Buffer {
             }
         });
 
+        self.parse_status.0.send(ParseStatus::Parsing).unwrap();
         match cx
             .background_executor()
             .block_with_timeout(self.sync_parse_timeout, parse_task)
@@ -1101,10 +1111,15 @@ impl Buffer {
         self.non_text_state_update_count += 1;
         self.syntax_map.lock().did_parse(syntax_snapshot);
         self.request_autoindent(cx);
+        self.parse_status.0.send(ParseStatus::Idle).unwrap();
         cx.emit(Event::Reparsed);
         cx.notify();
     }
 
+    pub fn parse_status(&self) -> watch::Receiver<ParseStatus> {
+        self.parse_status.1.clone()
+    }
+
     /// Assign to the buffer a set of diagnostics created by a given language server.
     pub fn update_diagnostics(
         &mut self,
@@ -2749,7 +2764,6 @@ impl BufferSnapshot {
             .map(|g| g.outline_config.as_ref().unwrap())
             .collect::<Vec<_>>();
 
-        let mut stack = Vec::<Range<usize>>::new();
         let mut items = Vec::new();
         while let Some(mat) = matches.peek() {
             let config = &configs[mat.grammar_index];
@@ -2767,6 +2781,9 @@ impl BufferSnapshot {
                 continue;
             }
 
+            let mut open_index = None;
+            let mut close_index = None;
+
             let mut buffer_ranges = Vec::new();
             for capture in mat.captures {
                 let node_is_name;
@@ -2778,6 +2795,12 @@ impl BufferSnapshot {
                 {
                     node_is_name = false;
                 } else {
+                    if Some(capture.index) == config.open_capture_ix {
+                        open_index = Some(capture.node.end_byte());
+                    } else if Some(capture.index) == config.close_capture_ix {
+                        close_index = Some(capture.node.start_byte());
+                    }
+
                     continue;
                 }
 
@@ -2850,22 +2873,45 @@ impl BufferSnapshot {
             }
 
             matches.advance();
-            while stack.last().map_or(false, |prev_range| {
-                prev_range.start > item_range.start || prev_range.end < item_range.end
-            }) {
-                stack.pop();
-            }
-            stack.push(item_range.clone());
 
             items.push(OutlineItem {
-                depth: stack.len() - 1,
-                range: self.anchor_after(item_range.start)..self.anchor_before(item_range.end),
+                depth: 0, // We'll calculate the depth later
+                range: item_range,
                 text,
                 highlight_ranges,
                 name_ranges,
-            })
+                body_range: open_index.zip(close_index).map(|(start, end)| start..end),
+            });
         }
-        Some(items)
+
+        items.sort_by_key(|item| (item.range.start, Reverse(item.range.end)));
+
+        // Assign depths based on containment relationships and convert to anchors.
+        let mut item_ends_stack = Vec::<usize>::new();
+        let mut anchor_items = Vec::new();
+        for item in items {
+            while let Some(last_end) = item_ends_stack.last().copied() {
+                if last_end < item.range.end {
+                    item_ends_stack.pop();
+                } else {
+                    break;
+                }
+            }
+
+            anchor_items.push(OutlineItem {
+                depth: item_ends_stack.len(),
+                range: self.anchor_after(item.range.start)..self.anchor_before(item.range.end),
+                text: item.text,
+                highlight_ranges: item.highlight_ranges,
+                name_ranges: item.name_ranges,
+                body_range: item.body_range.map(|body_range| {
+                    self.anchor_after(body_range.start)..self.anchor_before(body_range.end)
+                }),
+            });
+            item_ends_stack.push(item.range.end);
+        }
+
+        Some(anchor_items)
     }
 
     /// For each grammar in the language, runs the provided

crates/language/src/buffer_tests.rs ๐Ÿ”—

@@ -2615,7 +2615,8 @@ fn rust_lang() -> Language {
             "impl" @context
             trait: (_)? @name
             "for"? @context
-            type: (_) @name) @item
+            type: (_) @name
+            body: (_ "{" (_)* "}")) @item
         (function_item
             "fn" @context
             name: (_) @name) @item

crates/language/src/language.rs ๐Ÿ”—

@@ -867,6 +867,8 @@ pub struct OutlineConfig {
     pub name_capture_ix: u32,
     pub context_capture_ix: Option<u32>,
     pub extra_context_capture_ix: Option<u32>,
+    pub open_capture_ix: Option<u32>,
+    pub close_capture_ix: Option<u32>,
 }
 
 #[derive(Debug)]
@@ -1050,6 +1052,8 @@ impl Language {
         let mut name_capture_ix = None;
         let mut context_capture_ix = None;
         let mut extra_context_capture_ix = None;
+        let mut open_capture_ix = None;
+        let mut close_capture_ix = None;
         get_capture_indices(
             &query,
             &mut [
@@ -1057,6 +1061,8 @@ impl Language {
                 ("name", &mut name_capture_ix),
                 ("context", &mut context_capture_ix),
                 ("context.extra", &mut extra_context_capture_ix),
+                ("open", &mut open_capture_ix),
+                ("close", &mut close_capture_ix),
             ],
         );
         if let Some((item_capture_ix, name_capture_ix)) = item_capture_ix.zip(name_capture_ix) {
@@ -1066,6 +1072,8 @@ impl Language {
                 name_capture_ix,
                 context_capture_ix,
                 extra_context_capture_ix,
+                open_capture_ix,
+                close_capture_ix,
             });
         }
         Ok(self)

crates/language/src/outline.rs ๐Ÿ”—

@@ -23,6 +23,7 @@ pub struct OutlineItem<T> {
     pub text: String,
     pub highlight_ranges: Vec<(Range<usize>, HighlightStyle)>,
     pub name_ranges: Vec<Range<usize>>,
+    pub body_range: Option<Range<T>>,
 }
 
 impl<T> Outline<T> {

crates/languages/src/rust/outline.scm ๐Ÿ”—

@@ -16,7 +16,8 @@
     "impl" @context
     trait: (_)? @name
     "for"? @context
-    type: (_) @name) @item
+    type: (_) @name
+    body: (_ "{" @open (_)* "}" @close)) @item
 
 (trait_item
     (visibility_modifier)? @context

crates/multi_buffer/src/multi_buffer.rs ๐Ÿ”—

@@ -3639,6 +3639,12 @@ impl MultiBufferSnapshot {
                         text: item.text,
                         highlight_ranges: item.highlight_ranges,
                         name_ranges: item.name_ranges,
+                        body_range: item.body_range.and_then(|body_range| {
+                            Some(
+                                self.anchor_in_excerpt(*excerpt_id, body_range.start)?
+                                    ..self.anchor_in_excerpt(*excerpt_id, body_range.end)?,
+                            )
+                        }),
                     })
                 })
                 .collect(),
@@ -3668,6 +3674,12 @@ impl MultiBufferSnapshot {
                         text: item.text,
                         highlight_ranges: item.highlight_ranges,
                         name_ranges: item.name_ranges,
+                        body_range: item.body_range.and_then(|body_range| {
+                            Some(
+                                self.anchor_in_excerpt(excerpt_id, body_range.start)?
+                                    ..self.anchor_in_excerpt(excerpt_id, body_range.end)?,
+                            )
+                        }),
                     })
                 })
                 .collect(),

crates/project/src/project.rs ๐Ÿ”—

@@ -8403,6 +8403,37 @@ impl Project {
         })
     }
 
+    /// Attempts to find a `ProjectPath` corresponding to the given full path.
+    ///
+    /// This method iterates through all worktrees in the project, trying to match
+    /// the given full path against each worktree's root name. If a match is found,
+    /// it returns a `ProjectPath` containing the worktree ID and the relative path
+    /// within that worktree.
+    ///
+    /// # Arguments
+    ///
+    /// * `full_path` - A reference to a `Path` representing the full path to resolve.
+    /// * `cx` - A reference to the `AppContext`.
+    ///
+    /// # Returns
+    ///
+    /// Returns `Some(ProjectPath)` if a matching worktree is found, otherwise `None`.
+    pub fn project_path_for_full_path(
+        &self,
+        full_path: &Path,
+        cx: &AppContext,
+    ) -> Option<ProjectPath> {
+        self.worktrees.iter().find_map(|worktree| {
+            let worktree = worktree.upgrade()?;
+            let worktree_root_name = worktree.read(cx).root_name();
+            let relative_path = full_path.strip_prefix(worktree_root_name).ok()?;
+            Some(ProjectPath {
+                worktree_id: worktree.read(cx).id(),
+                path: relative_path.into(),
+            })
+        })
+    }
+
     pub fn get_workspace_root(
         &self,
         project_path: &ProjectPath,

crates/project_symbols/src/project_symbols.rs ๐Ÿ”—

@@ -131,7 +131,8 @@ impl PickerDelegate for ProjectSymbolsDelegate {
                         workspace.active_pane().clone()
                     };
 
-                    let editor = workspace.open_project_item::<Editor>(pane, buffer, cx);
+                    let editor =
+                        workspace.open_project_item::<Editor>(pane, buffer, true, true, cx);
 
                     editor.update(cx, |editor, cx| {
                         editor.change_selections(Some(Autoscroll::center()), cx, |s| {

crates/search/src/project_search.rs ๐Ÿ”—

@@ -716,7 +716,7 @@ impl ProjectSearchView {
 
         let model = cx.new_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
         let search = cx.new_view(|cx| ProjectSearchView::new(model, cx, None));
-        workspace.add_item_to_active_pane(Box::new(search.clone()), None, cx);
+        workspace.add_item_to_active_pane(Box::new(search.clone()), None, true, cx);
         search.update(cx, |search, cx| {
             search
                 .included_files_editor
@@ -768,6 +768,7 @@ impl ProjectSearchView {
                 workspace.add_item_to_active_pane(
                     Box::new(cx.new_view(|cx| ProjectSearchView::new(model, cx, None))),
                     None,
+                    true,
                     cx,
                 );
             }
@@ -800,7 +801,7 @@ impl ProjectSearchView {
         });
 
         let search = if let Some(existing) = existing {
-            workspace.activate_item(&existing, cx);
+            workspace.activate_item(&existing, true, true, cx);
             existing
         } else {
             let settings = cx
@@ -817,7 +818,7 @@ impl ProjectSearchView {
             let model = cx.new_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
             let view = cx.new_view(|cx| ProjectSearchView::new(model, cx, settings));
 
-            workspace.add_item_to_active_pane(Box::new(view.clone()), None, cx);
+            workspace.add_item_to_active_pane(Box::new(view.clone()), None, true, cx);
             view
         };
 

crates/tasks_ui/src/lib.rs ๐Ÿ”—

@@ -340,7 +340,7 @@ mod tests {
             workspace
                 .update(cx, |workspace, cx| {
                     // Now, let's switch the active item to .ts file.
-                    workspace.activate_item(&editor1, cx);
+                    workspace.activate_item(&editor1, true, true, cx);
                     task_context(workspace, cx)
                 })
                 .await,

crates/text/src/anchor.rs ๐Ÿ”—

@@ -136,6 +136,7 @@ where
 
 pub trait AnchorRangeExt {
     fn cmp(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> Ordering;
+    fn intersects(&self, other: &Range<Anchor>, buffer: &BufferSnapshot) -> bool;
 }
 
 impl AnchorRangeExt for Range<Anchor> {
@@ -145,4 +146,8 @@ impl AnchorRangeExt for Range<Anchor> {
             ord => ord,
         }
     }
+
+    fn intersects(&self, other: &Range<Anchor>, buffer: &BufferSnapshot) -> bool {
+        self.start.cmp(&other.end, buffer).is_lt() && other.start.cmp(&self.end, buffer).is_lt()
+    }
 }

crates/welcome/src/welcome.rs ๐Ÿ”—

@@ -32,7 +32,7 @@ pub fn init(cx: &mut AppContext) {
     cx.observe_new_views(|workspace: &mut Workspace, _cx| {
         workspace.register_action(|workspace, _: &Welcome, cx| {
             let welcome_page = WelcomePage::new(workspace, cx);
-            workspace.add_item_to_active_pane(Box::new(welcome_page), None, cx)
+            workspace.add_item_to_active_pane(Box::new(welcome_page), None, true, cx)
         });
         workspace
             .register_action(|_workspace, _: &ResetHints, cx| MultibufferHint::set_count(0, cx));

crates/workspace/src/workspace.rs ๐Ÿ”—

@@ -2380,9 +2380,17 @@ impl Workspace {
         &mut self,
         item: Box<dyn ItemHandle>,
         destination_index: Option<usize>,
+        focus_item: bool,
         cx: &mut WindowContext,
     ) {
-        self.add_item(self.active_pane.clone(), item, destination_index, cx)
+        self.add_item(
+            self.active_pane.clone(),
+            item,
+            destination_index,
+            false,
+            focus_item,
+            cx,
+        )
     }
 
     pub fn add_item(
@@ -2390,6 +2398,8 @@ impl Workspace {
         pane: View<Pane>,
         item: Box<dyn ItemHandle>,
         destination_index: Option<usize>,
+        activate_pane: bool,
+        focus_item: bool,
         cx: &mut WindowContext,
     ) {
         if let Some(text) = item.telemetry_event_text(cx) {
@@ -2399,7 +2409,7 @@ impl Workspace {
         }
 
         pane.update(cx, |pane, cx| {
-            pane.add_item(item, true, true, destination_index, cx)
+            pane.add_item(item, activate_pane, focus_item, destination_index, cx)
         });
     }
 
@@ -2410,7 +2420,7 @@ impl Workspace {
         cx: &mut ViewContext<Self>,
     ) {
         let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
-        self.add_item(new_pane, item, None, cx);
+        self.add_item(new_pane, item, None, true, true, cx);
     }
 
     pub fn open_abs_path(
@@ -2565,6 +2575,8 @@ impl Workspace {
         &mut self,
         pane: View<Pane>,
         project_item: Model<T::Item>,
+        activate_pane: bool,
+        focus_item: bool,
         cx: &mut ViewContext<Self>,
     ) -> View<T>
     where
@@ -2577,7 +2589,7 @@ impl Workspace {
             .and_then(|entry_id| pane.read(cx).item_for_entry(entry_id, cx))
             .and_then(|item| item.downcast())
         {
-            self.activate_item(&item, cx);
+            self.activate_item(&item, activate_pane, focus_item, cx);
             return item;
         }
 
@@ -2596,7 +2608,14 @@ impl Workspace {
             pane.set_preview_item_id(Some(item.item_id()), cx)
         });
 
-        self.add_item(pane, Box::new(item.clone()), destination_index, cx);
+        self.add_item(
+            pane,
+            Box::new(item.clone()),
+            destination_index,
+            activate_pane,
+            focus_item,
+            cx,
+        );
         item
     }
 
@@ -2608,14 +2627,22 @@ impl Workspace {
         }
     }
 
-    pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut WindowContext) -> bool {
+    pub fn activate_item(
+        &mut self,
+        item: &dyn ItemHandle,
+        activate_pane: bool,
+        focus_item: bool,
+        cx: &mut WindowContext,
+    ) -> bool {
         let result = self.panes.iter().find_map(|pane| {
             pane.read(cx)
                 .index_for_item(item)
                 .map(|ix| (pane.clone(), ix))
         });
         if let Some((pane, ix)) = result {
-            pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
+            pane.update(cx, |pane, cx| {
+                pane.activate_item(ix, activate_pane, focus_item, cx)
+            });
             true
         } else {
             false
@@ -5568,7 +5595,7 @@ mod tests {
             item
         });
         workspace.update(cx, |workspace, cx| {
-            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, cx);
+            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
         });
         item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
 
@@ -5580,7 +5607,7 @@ mod tests {
             item
         });
         workspace.update(cx, |workspace, cx| {
-            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, cx);
+            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
         });
         item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
         item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
@@ -5594,7 +5621,7 @@ mod tests {
             item
         });
         workspace.update(cx, |workspace, cx| {
-            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, cx);
+            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
         });
         item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
         item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
@@ -5638,7 +5665,7 @@ mod tests {
 
         // Add an item to an empty pane
         workspace.update(cx, |workspace, cx| {
-            workspace.add_item_to_active_pane(Box::new(item1), None, cx)
+            workspace.add_item_to_active_pane(Box::new(item1), None, true, cx)
         });
         project.update(cx, |project, cx| {
             assert_eq!(
@@ -5652,7 +5679,7 @@ mod tests {
 
         // Add a second item to a non-empty pane
         workspace.update(cx, |workspace, cx| {
-            workspace.add_item_to_active_pane(Box::new(item2), None, cx)
+            workspace.add_item_to_active_pane(Box::new(item2), None, true, cx)
         });
         assert_eq!(cx.window_title().as_deref(), Some("two.txt โ€” root1"));
         project.update(cx, |project, cx| {
@@ -5707,7 +5734,7 @@ mod tests {
         // When there are no dirty items, there's nothing to do.
         let item1 = cx.new_view(|cx| TestItem::new(cx));
         workspace.update(cx, |w, cx| {
-            w.add_item_to_active_pane(Box::new(item1.clone()), None, cx)
+            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx)
         });
         let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
         assert!(task.await.unwrap());
@@ -5721,8 +5748,8 @@ mod tests {
                 .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
         });
         workspace.update(cx, |w, cx| {
-            w.add_item_to_active_pane(Box::new(item2.clone()), None, cx);
-            w.add_item_to_active_pane(Box::new(item3.clone()), None, cx);
+            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
+            w.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
         });
         let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
         cx.executor().run_until_parked();
@@ -5762,8 +5789,8 @@ mod tests {
                 .with_serialize(|| Some(Task::ready(Ok(()))))
         });
         workspace.update(cx, |w, cx| {
-            w.add_item_to_active_pane(Box::new(item1.clone()), None, cx);
-            w.add_item_to_active_pane(Box::new(item2.clone()), None, cx);
+            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
+            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
         });
         let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
         assert!(task.await.unwrap());
@@ -5801,10 +5828,10 @@ mod tests {
                 .with_project_items(&[TestProjectItem::new_untitled(cx)])
         });
         let pane = workspace.update(cx, |workspace, cx| {
-            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, cx);
-            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, cx);
-            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, cx);
-            workspace.add_item_to_active_pane(Box::new(item4.clone()), None, cx);
+            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
+            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
+            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
+            workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, cx);
             workspace.active_pane().clone()
         });
 
@@ -5926,9 +5953,9 @@ mod tests {
         //     multi-entry items:   (3, 4)
         let left_pane = workspace.update(cx, |workspace, cx| {
             let left_pane = workspace.active_pane().clone();
-            workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, cx);
+            workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, cx);
             for item in single_entry_items {
-                workspace.add_item_to_active_pane(Box::new(item), None, cx);
+                workspace.add_item_to_active_pane(Box::new(item), None, true, cx);
             }
             left_pane.update(cx, |pane, cx| {
                 pane.activate_item(2, true, true, cx);
@@ -5999,7 +6026,7 @@ mod tests {
         });
         let item_id = item.entity_id();
         workspace.update(cx, |workspace, cx| {
-            workspace.add_item_to_active_pane(Box::new(item.clone()), None, cx);
+            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
         });
 
         // Autosave on window change.
@@ -6084,7 +6111,7 @@ mod tests {
 
         // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
         workspace.update(cx, |workspace, cx| {
-            workspace.add_item_to_active_pane(Box::new(item.clone()), None, cx);
+            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
         });
         item.update(cx, |item, cx| {
             item.project_items[0].update(cx, |item, _| {
@@ -6122,7 +6149,7 @@ mod tests {
         let toolbar_notify_count = Rc::new(RefCell::new(0));
 
         workspace.update(cx, |workspace, cx| {
-            workspace.add_item_to_active_pane(Box::new(item.clone()), None, cx);
+            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
             let toolbar_notification_count = toolbar_notify_count.clone();
             cx.observe(&toolbar, move |_, _, _| {
                 *toolbar_notification_count.borrow_mut() += 1

crates/zed/src/zed.rs ๐Ÿ”—

@@ -670,7 +670,7 @@ fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
                             })
                         });
 
-                        workspace.add_item_to_active_pane(Box::new(editor), None, cx);
+                        workspace.add_item_to_active_pane(Box::new(editor), None, true, cx);
                     })
                     .log_err();
             })
@@ -889,7 +889,9 @@ fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Works
                 });
                 workspace.add_item_to_active_pane(
                     Box::new(cx.new_view(|cx| Editor::for_multibuffer(buffer, Some(project), true, cx))),
-                    None,cx,
+                    None,
+                    true,
+                    cx,
                 );
             }).log_err()?;
 
@@ -924,6 +926,7 @@ fn open_bundled_file(
                             Editor::for_multibuffer(buffer, Some(project.clone()), true, cx)
                         })),
                         None,
+                        true,
                         cx,
                     );
                 })