diff --git a/Cargo.lock b/Cargo.lock
index b58716cd57..c638ebafa4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -412,6 +412,7 @@ dependencies = [
"parking_lot",
"paths",
"picker",
+ "pretty_assertions",
"project",
"proto",
"rand 0.8.5",
diff --git a/assets/icons/diff.svg b/assets/icons/diff.svg
new file mode 100644
index 0000000000..ca43c379da
--- /dev/null
+++ b/assets/icons/diff.svg
@@ -0,0 +1 @@
+
diff --git a/assets/prompts/edit_workflow.hbs b/assets/prompts/edit_workflow.hbs
index c558bc20d0..99a594cdd8 100644
--- a/assets/prompts/edit_workflow.hbs
+++ b/assets/prompts/edit_workflow.hbs
@@ -1,85 +1,33 @@
-# Code Change Workflow
+The user of a code editor wants to make a change to their codebase.
+You must describe the change using the following XML structure:
-Your task is to guide the user through code changes using a series of steps. Each step should describe a high-level change, which can consist of multiple edits to distinct locations in the codebase.
-
-## Output Example
-
-Provide output as XML, with the following format:
-
-
-Update the Person struct to store an age
-
-```rust
-struct Person {
- // existing fields...
- age: u8,
- height: f32,
- // existing fields...
-}
-
-impl Person {
- fn age(&self) -> u8 {
- self.age
- }
-}
-```
-
-
-src/person.rs
-insert_before
-height: f32,
-Add the age field
-
-
-
-src/person.rs
-insert_after
-impl Person {
-Add the age getter
-
-
-
-## Output Format
-
-First, each `` must contain a written description of the change that should be made. The description should begin with a high-level overview, and can contain markdown code blocks as well. The description should be self-contained and actionable.
-
-After the description, each `` must contain one or more `` tags, each of which refer to a specific range in a source file. Each `` tag must contain the following child tags:
-
-### `` (required)
-
-This tag contains the path to the file that will be changed. It can be an existing path, or a path that should be created.
-
-### `` (optional)
-
-This tag contains a search string to locate in the source file, e.g. `pub fn baz() {`. If not provided, the new content will be inserted at the top of the file. Make sure to produce a string that exists in the source file and that isn't ambiguous. When there's ambiguity, add more lines to the search to eliminate it.
-
-### `` (required)
-
-This tag contains a single-line description of the edit that should be made at the given location.
-
-### `` (required)
-
-This tag indicates what type of change should be made, relative to the given location. It can be one of the following:
-- `update`: Rewrites the specified string entirely based on the given description.
-- `create`: Creates a new file with the given path based on the provided description.
-- `insert_before`: Inserts new text based on the given description before the specified search string.
-- `insert_after`: Inserts new text based on the given description after the specified search string.
-- `delete`: Deletes the specified string from the containing file.
+- - A group of related code changes.
+ Child tags:
+ - (required) - A high-level description of the changes. This should be as short
+ as possible, possibly using common abbreviations.
+ - (1 or more) - An edit to make at a particular range within a file.
+ Includes the following child tags:
+ - (required) - The path to the file that will be changed.
+ - (optional) - An arbitrarily-long comment that describes the purpose
+ of this edit.
+ - (optional) - An excerpt from the file's current contents that uniquely
+ identifies a range within the file where the edit should occur. If this tag is not
+ specified, then the entire file will be used as the range.
+ - (required) - The new text to insert into the file.
+ - (required) - The type of change that should occur at the given range
+ of the file. Must be one of the following values:
+ - `update`: Replaces the entire range with the new text.
+ - `insert_before`: Inserts the new text before the range.
+ - `insert_after`: Inserts new text after the range.
+ - `create`: Creates a new file with the given path and the new text.
+ - `delete`: Deletes the specified range from the file.
-- There's no need to describe *what* to do, just *where* to do it.
-- Only reference locations that actually exist (unless you're creating a file).
-- If creating a file, assume any subsequent updates are included at the time of creation.
-- Don't create and then update a file. Always create new files in one hot.
-- Prefer multiple edits to smaller regions, as opposed to one big edit to a larger region.
-- Don't produce edits that intersect each other. In that case, merge them into a bigger edit.
-- Never nest an edit with another edit. Never include CDATA. All edits are leaf nodes.
-- Descriptions are required for all edits except delete.
-- When generating multiple edits, ensure the descriptions are specific to each individual operation.
-- Avoid referring to the search string in the description. Focus on the change to be made, not the location where it's made. That's implicit with the `search` string you provide.
-- Don't generate multiple edits at the same location. Instead, combine them together in a single edit with a succinct combined description.
+- Never provide multiple edits whose ranges intersect each other. Instead, merge them into one edit.
+- Prefer multiple edits to smaller, disjoint ranges, rather than one edit to a larger range.
+- There's no need to escape angle brackets within XML tags.
- Always ensure imports are added if you're referencing symbols that are not in scope.
@@ -124,189 +72,137 @@ Update all shapes to store their origin as an (x, y) tuple and implement Display
We'll need to update both the rectangle and circle modules.
-
-Add origin fields to both shape types.
-
-```rust
-struct Rectangle {
- // existing fields ...
- origin: (f64, f64),
-}
-```
-
-```rust
-struct Circle {
- // existing fields ...
- origin: (f64, f64),
-}
-```
+
+Add origins and display impls to shapes
+
+src/shapes/rectangle.rs
+Add the origin field to Rectangle struct
+insert_after
+
+pub struct Rectangle {
+
+
+origin: (f64, f64),
+
+
src/shapes/rectangle.rs
-insert_before
-
- width: f64,
- height: f64,
-
-Add the origin field to Rectangle
+Update the Rectangle's new function to take an origin parameter
+update
+
+fn new(width: f64, height: f64) -> Self {
+ Rectangle { width, height }
+}
+
+
+fn new(origin: (f64, f64), width: f64, height: f64) -> Self {
+ Rectangle { origin, width, height }
+}
+
src/shapes/circle.rs
-insert_before
-
+Add the origin field to Circle struct
+insert_after
+
+pub struct Circle {
radius: f64,
-
-Add the origin field to Circle
-
-
-
-Update both shape's constructors to take an origin.
-
-
-src/shapes/rectangle.rs
-update
-
- fn new(width: f64, height: f64) -> Self {
- Rectangle { width, height }
- }
-
-Update the Rectangle new function to take an origin
+
+
+ origin: (f64, f64),
+
src/shapes/circle.rs
+Update the Circle's new function to take an origin parameter
update
-
- fn new(radius: f64) -> Self {
- Circle { radius }
- }
-
-Update the Circle new function to take an origin
+
+fn new(radius: f64) -> Self {
+ Circle { radius }
+}
+
+
+fn new(origin: (f64, f64), radius: f64) -> Self {
+ Circle { origin, radius }
+}
+
-
-Implement Display for both shapes
-
src/shapes/rectangle.rs
+Add an import for the std::fmt module
insert_before
-
+
struct Rectangle {
-
-Add an import for the `std::fmt` module
+
+
+use std::fmt;
+
+
src/shapes/rectangle.rs
+Add a Display implementation for Rectangle
insert_after
-
+
Rectangle { width, height }
}
}
-
-Add a Display implementation for Rectangle
+
+
+impl fmt::Display for Rectangle {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.format_struct(f, "Rectangle")
+ .field("origin", &self.origin)
+ .field("width", &self.width)
+ .field("height", &self.height)
+ .finish()
+ }
+}
+
src/shapes/circle.rs
-insert_before
-
-struct Circle {
-
Add an import for the `std::fmt` module
+insert_before
+
+struct Circle {
+
+
+use std::fmt;
+
src/shapes/circle.rs
+Add a Display implementation for Circle
insert_after
-
+
Circle { radius }
}
}
-
-Add a Display implementation for Circle
+
+
+impl fmt::Display for Rectangle {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.format_struct(f, "Rectangle")
+ .field("origin", &self.origin)
+ .field("width", &self.width)
+ .field("height", &self.height)
+ .finish()
+ }
+}
+
-
+
+
-
-
-
-```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) {
- todo!()
- }
-}
-```
-
-Let's print all the user information and delete the email field.
-
-
-
-
-Update the 'print_info' method to print user information
-
-```rust
-impl User {
- // ... other methods ...
-
- pub fn print_info(&self) {
- println!("Name: {name}, Age: {age}", name = self.name, age = self.age);
- }
-}
-```
-
-
-src/user.rs
-update
-
- pub fn print_info(&self) {
- todo!()
- }
-
-Print all the user information
-
-
-
-
-Remove the 'email' field from the User struct
-
-
-src/user.rs
-delete
-
-email: String,
-
-
-
-
-src/user.rs
-update
-
-fn new(name: String, age: u32, email: String) -> Self {
- User { name, age, email }
-}
-
-Remove email parameter from new method
-
-
-
-
-
-You should think step by step. When possible, produce smaller, coherent logical steps as opposed to one big step that combines lots of heterogeneous edits.
-
diff --git a/assets/prompts/step_resolution.hbs b/assets/prompts/step_resolution.hbs
deleted file mode 100644
index 10bbdec81e..0000000000
--- a/assets/prompts/step_resolution.hbs
+++ /dev/null
@@ -1,496 +0,0 @@
-
-Your task is to map a step from a workflow to locations in source code where code needs to be changed to fulfill that step.
-Given a workflow containing background context plus a series of tags, you will resolve *one* of these step tags to resolve to one or more locations in the code.
-With each location, you will produce a brief, one-line description of the changes to be made.
-
-
-- There's no need to describe *what* to do, just *where* to do it.
-- Only reference locations that actually exist (unless you're creating a file).
-- If creating a file, assume any subsequent updates are included at the time of creation.
-- Don't create and then update a file. Always create new files in shot.
-- Prefer updating symbols lower in the syntax tree if possible.
-- Never include suggestions on a parent symbol and one of its children in the same suggestions block.
-- Never nest an operation with another operation or include CDATA or other content. All suggestions are leaf nodes.
-- Descriptions are required for all suggestions except delete.
-- When generating multiple suggestions, ensure the descriptions are specific to each individual operation.
-- Avoid referring to the location in the description. Focus on the change to be made, not the location where it's made. That's implicit with the symbol you provide.
-- Don't generate multiple suggestions at the same location. Instead, combine them together in a single operation with a succinct combined description.
-- To add imports respond with a suggestion where the `"symbol"` key is set to `"#imports"`
-
-
-
-
-
-
-
-```rs src/rectangle.rs
-struct Rectangle {
- width: f64,
- height: f64,
-}
-
-impl Rectangle {
- fn new(width: f64, height: f64) -> Self {
- Rectangle { width, height }
- }
-}
-```
-
-We need to add methods to calculate the area and perimeter of the rectangle. Can you help with that?
-
-
-Sure, I can help with that!
-
-Add new methods 'calculate_area' and 'calculate_perimeter' to the Rectangle struct
-Implement the 'Display' trait for the Rectangle struct
-
-
-
-
-Add new methods 'calculate_area' and 'calculate_perimeter' to the Rectangle struct
-
-
-
-{
- "title": "Add Rectangle methods",
- "suggestions": [
- {
- "kind": "AppendChild",
- "path": "src/shapes.rs",
- "symbol": "impl Rectangle",
- "description": "Add calculate_area method"
- },
- {
- "kind": "AppendChild",
- "path": "src/shapes.rs",
- "symbol": "impl Rectangle",
- "description": "Add calculate_perimeter method"
- }
- ]
-}
-
-
-
-{
- "title": "Add Rectangle methods",
- "suggestions": [
- {
- "kind": "AppendChild",
- "path": "src/shapes.rs",
- "symbol": "impl Rectangle",
- "description": "Add calculate area and perimeter methods"
- }
- ]
-}
-
-
-
-Implement the 'Display' trait for the Rectangle struct
-
-
-
-
-
-
-
-```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);
- }
-}
-```
-
-
-Certainly!
-Update the 'print_info' method to use formatted output
-Remove the 'email' field from the User struct
-
-
-
-
-Update the 'print_info' method to use formatted output
-
-
-
-
-
-Remove the 'email' field from the User struct
-
-
-
-
-
-
-
-
-```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);
- }
-}
-```
-
-
-Add a 'use std::fmt;' statement at the beginning of the file
-Add a new method 'start_engine' in the Vehicle impl block
-
-
-
-
-Add a 'use std::fmt;' statement at the beginning of the file
-
-
-
-
-
-Add a new method 'start_engine' in the Vehicle impl block
-
-
-
-
-
-
-
-
-```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;
- }
-}
-```
-
-
-Make salary an f32
-Remove the 'department' field and update the 'print_details' method
-
-
-
-
-Make salary an f32
-
-
-
-{
- "title": "Change salary to f32",
- "suggestions": [
- {
- "kind": "Update",
- "path": "src/employee.rs",
- "symbol": "struct Employee",
- "description": "Change the type of salary to an f32"
- },
- {
- "kind": "Update",
- "path": "src/employee.rs",
- "symbol": "struct Employee salary",
- "description": "Change the type to an f32"
- }
- ]
-}
-
-
-
-{
- "title": "Change salary to f32",
- "suggestions": [
- {
- "kind": "Update",
- "path": "src/employee.rs",
- "symbol": "struct Employee salary",
- "description": "Change the type to an f32"
- }
- ]
-}
-
-
-
-Remove the 'department' field and update the 'print_details' method
-
-
-
-
-
-
-
-
-```rs src/game.rs
-struct Player {
- name: String,
- health: i32,
- pub score: u32,
-}
-
-impl Player {
- pub fn new(name: String) -> Self {
- Player { name, health: 100, score: 0 }
- }
-}
-
-struct Game {
- players: Vec,
-}
-
-impl Game {
- fn new() -> Self {
- Game { players: Vec::new() }
- }
-}
-```
-
-
-Add a 'level' field to Player and update the 'new' method
-
-
-
-
-Add a 'level' field to Player and update the 'new' method
-
-
-
-
-
-
-
-
-```rs src/config.rs
-use std::collections::HashMap;
-
-struct Config {
- settings: HashMap,
-}
-
-impl Config {
- fn new() -> Self {
- Config { settings: HashMap::new() }
- }
-}
-```
-
-
-Add a 'load_from_file' method to Config and import necessary modules
-
-
-
-
-Add a 'load_from_file' method to Config and import necessary modules
-
-
-
-
-
-
-
-
-```rs src/database.rs
-pub(crate) struct Database {
- connection: Connection,
-}
-
-impl Database {
- fn new(url: &str) -> Result {
- let connection = Connection::connect(url)?;
- Ok(Database { connection })
- }
-
- async fn query(&self, sql: &str) -> Result, Error> {
- self.connection.query(sql, &[])
- }
-}
-```
-
-
-Add error handling to the 'query' method and create a custom error type
-
-
-
-
-Add error handling to the 'query' method and create a custom error type
-
-
-
-
-
-
-Now generate the suggestions for the following step:
-
-
-{{{workflow_context}}}
-
-
-
-{{{step_to_resolve}}}
-
diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml
index 9e61eee18a..21153b6fcc 100644
--- a/crates/assistant/Cargo.toml
+++ b/crates/assistant/Cargo.toml
@@ -97,6 +97,7 @@ language = { workspace = true, features = ["test-support"] }
language_model = { workspace = true, features = ["test-support"] }
languages = { workspace = true, features = ["test-support"] }
log.workspace = true
+pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
serde_json_lenient.workspace = true
diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs
index 9cc63af5a1..e1e574744f 100644
--- a/crates/assistant/src/assistant.rs
+++ b/crates/assistant/src/assistant.rs
@@ -6,6 +6,7 @@ mod context;
pub mod context_store;
mod inline_assistant;
mod model_selector;
+mod patch;
mod prompt_library;
mod prompts;
mod slash_command;
@@ -14,7 +15,6 @@ pub mod slash_command_settings;
mod streaming_diff;
mod terminal_inline_assistant;
mod tools;
-mod workflow;
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
use assistant_settings::AssistantSettings;
@@ -35,11 +35,13 @@ use language_model::{
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
};
pub(crate) use model_selector::*;
+pub use patch::*;
pub use prompts::PromptBuilder;
use prompts::PromptLoadingParams;
use semantic_index::{CloudEmbeddingProvider, SemanticDb};
use serde::{Deserialize, Serialize};
use settings::{update_settings_file, Settings, SettingsStore};
+use slash_command::workflow_command::WorkflowSlashCommand;
use slash_command::{
auto_command, cargo_workspace_command, context_server_command, default_command, delta_command,
diagnostics_command, docs_command, fetch_command, file_command, now_command, project_command,
@@ -50,7 +52,6 @@ use std::path::PathBuf;
use std::sync::Arc;
pub(crate) use streaming_diff::*;
use util::ResultExt;
-pub use workflow::*;
use crate::slash_command_settings::SlashCommandSettings;
@@ -393,12 +394,25 @@ fn register_slash_commands(prompt_builder: Option>, cx: &mut
slash_command_registry.register_command(now_command::NowSlashCommand, false);
slash_command_registry.register_command(diagnostics_command::DiagnosticsSlashCommand, true);
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
+ slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
if let Some(prompt_builder) = prompt_builder {
- slash_command_registry.register_command(
- workflow_command::WorkflowSlashCommand::new(prompt_builder.clone()),
- true,
- );
+ cx.observe_global::({
+ let slash_command_registry = slash_command_registry.clone();
+ let prompt_builder = prompt_builder.clone();
+ move |cx| {
+ if AssistantSettings::get_global(cx).are_live_diffs_enabled(cx) {
+ slash_command_registry.register_command(
+ workflow_command::WorkflowSlashCommand::new(prompt_builder.clone()),
+ true,
+ );
+ } else {
+ slash_command_registry.unregister_command_by_name(WorkflowSlashCommand::NAME);
+ }
+ }
+ })
+ .detach();
+
cx.observe_flag::({
let slash_command_registry = slash_command_registry.clone();
move |is_enabled, _cx| {
diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs
index a3e189287f..5c20caa863 100644
--- a/crates/assistant/src/assistant_panel.rs
+++ b/crates/assistant/src/assistant_panel.rs
@@ -11,12 +11,12 @@ use crate::{
},
slash_command_picker,
terminal_inline_assistant::TerminalInlineAssistant,
- Assist, CacheStatus, ConfirmCommand, Content, Context, ContextEvent, ContextId, ContextStore,
- ContextStoreEvent, CopyCode, CycleMessageRole, DeployHistory, DeployPromptLibrary,
- InlineAssistId, InlineAssistant, InsertDraggedFiles, InsertIntoEditor, Message, MessageId,
- MessageMetadata, MessageStatus, ModelPickerDelegate, ModelSelector, NewContext,
- PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata,
- SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector, WorkflowStepResolution,
+ Assist, AssistantPatch, AssistantPatchStatus, CacheStatus, ConfirmCommand, Content, Context,
+ ContextEvent, ContextId, ContextStore, ContextStoreEvent, CopyCode, CycleMessageRole,
+ DeployHistory, DeployPromptLibrary, InlineAssistant, InsertDraggedFiles, InsertIntoEditor,
+ Message, MessageId, MessageMetadata, MessageStatus, ModelPickerDelegate, ModelSelector,
+ NewContext, PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection,
+ RemoteContextMetadata, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
};
use anyhow::Result;
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
@@ -26,11 +26,12 @@ use collections::{BTreeSet, HashMap, HashSet};
use editor::{
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
display_map::{
- BlockDisposition, BlockId, BlockProperties, BlockStyle, Crease, CreaseMetadata,
- CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
+ BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, Crease,
+ CreaseMetadata, CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
},
- scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor},
- Anchor, Editor, EditorEvent, ExcerptRange, MultiBuffer, RowExt, ToOffset as _, ToPoint,
+ scroll::{Autoscroll, AutoscrollStrategy},
+ Anchor, Editor, EditorEvent, ProposedChangeLocation, ProposedChangesEditor, RowExt,
+ ToOffset as _, ToPoint,
};
use editor::{display_map::CreaseId, FoldPlaceholder};
use fs::Fs;
@@ -38,15 +39,14 @@ use futures::FutureExt;
use gpui::{
canvas, div, img, percentage, point, pulsating_between, size, Action, Animation, AnimationExt,
AnyElement, AnyView, AppContext, AsyncWindowContext, ClipboardEntry, ClipboardItem,
- Context as _, Empty, Entity, EntityId, EventEmitter, ExternalPaths, FocusHandle, FocusableView,
- FontWeight, InteractiveElement, IntoElement, Model, ParentElement, Pixels, ReadGlobal, Render,
- RenderImage, SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task,
- Transformation, UpdateGlobal, View, VisualContext, WeakView, WindowContext,
+ CursorStyle, Empty, Entity, EventEmitter, ExternalPaths, FocusHandle, FocusableView,
+ FontWeight, InteractiveElement, IntoElement, Model, ParentElement, Pixels, Render, RenderImage,
+ SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
+ UpdateGlobal, View, VisualContext, WeakView, WindowContext,
};
use indexed_docs::IndexedDocsStore;
use language::{
- language_settings::SoftWrap, BufferSnapshot, Capability, LanguageRegistry, LspAdapterDelegate,
- ToOffset,
+ language_settings::SoftWrap, BufferSnapshot, LanguageRegistry, LspAdapterDelegate, ToOffset,
};
use language_model::{
provider::cloud::PROVIDER_ID, LanguageModelProvider, LanguageModelProviderId,
@@ -65,7 +65,6 @@ use smol::stream::StreamExt;
use std::{
borrow::Cow,
cmp,
- collections::hash_map,
ops::{ControlFlow, Range},
path::PathBuf,
sync::Arc,
@@ -1444,61 +1443,16 @@ struct ScrollPosition {
cursor: Anchor,
}
-struct WorkflowStepViewState {
- header_block_id: CustomBlockId,
- header_crease_id: CreaseId,
- footer_block_id: Option,
- footer_crease_id: Option,
- assist: Option,
- resolution: Option>>,
+struct PatchViewState {
+ footer_block_id: CustomBlockId,
+ crease_id: CreaseId,
+ editor: Option,
+ update_task: Option>,
}
-impl WorkflowStepViewState {
- fn status(&self, cx: &AppContext) -> WorkflowStepStatus {
- if let Some(assist) = &self.assist {
- match assist.status(cx) {
- WorkflowAssistStatus::Idle => WorkflowStepStatus::Idle,
- WorkflowAssistStatus::Pending => WorkflowStepStatus::Pending,
- WorkflowAssistStatus::Done => WorkflowStepStatus::Done,
- WorkflowAssistStatus::Confirmed => WorkflowStepStatus::Confirmed,
- }
- } else if let Some(resolution) = self.resolution.as_deref() {
- match resolution {
- Err(err) => WorkflowStepStatus::Error(err),
- Ok(_) => WorkflowStepStatus::Idle,
- }
- } else {
- WorkflowStepStatus::Resolving
- }
- }
-}
-
-#[derive(Clone, Copy)]
-enum WorkflowStepStatus<'a> {
- Resolving,
- Error(&'a anyhow::Error),
- Idle,
- Pending,
- Done,
- Confirmed,
-}
-
-impl<'a> WorkflowStepStatus<'a> {
- pub(crate) fn is_confirmed(&self) -> bool {
- matches!(self, Self::Confirmed)
- }
-}
-
-#[derive(Debug, Eq, PartialEq)]
-struct ActiveWorkflowStep {
- range: Range,
- resolved: bool,
-}
-
-struct WorkflowAssist {
- editor: WeakView,
- editor_was_open: bool,
- assist_ids: Vec,
+struct PatchEditorState {
+ editor: WeakView,
+ opened_patch: AssistantPatch,
}
type MessageHeader = MessageMetadata;
@@ -1525,8 +1479,8 @@ pub struct ContextEditor {
pending_slash_command_blocks: HashMap, CustomBlockId>,
pending_tool_use_creases: HashMap, CreaseId>,
_subscriptions: Vec,
- workflow_steps: HashMap, WorkflowStepViewState>,
- active_workflow_step: Option,
+ patches: HashMap, PatchViewState>,
+ active_patch: Option>,
assistant_panel: WeakView,
last_error: Option,
show_accept_terms: bool,
@@ -1580,7 +1534,7 @@ impl ContextEditor {
];
let sections = context.read(cx).slash_command_output_sections().to_vec();
- let edit_step_ranges = context.read(cx).workflow_step_ranges().collect::>();
+ let patch_ranges = context.read(cx).patch_ranges().collect::>();
let mut this = Self {
context,
editor,
@@ -1596,8 +1550,8 @@ impl ContextEditor {
pending_slash_command_blocks: HashMap::default(),
pending_tool_use_creases: HashMap::default(),
_subscriptions,
- workflow_steps: HashMap::default(),
- active_workflow_step: None,
+ patches: HashMap::default(),
+ active_patch: None,
assistant_panel,
last_error: None,
show_accept_terms: false,
@@ -1607,7 +1561,7 @@ impl ContextEditor {
this.update_message_headers(cx);
this.update_image_blocks(cx);
this.insert_slash_command_output_sections(sections, false, cx);
- this.workflow_steps_updated(&Vec::new(), &edit_step_ranges, cx);
+ this.patches_updated(&Vec::new(), &patch_ranges, cx);
this
}
@@ -1642,134 +1596,28 @@ impl ContextEditor {
return;
}
- if !self.apply_active_workflow_step(cx) {
- self.last_error = None;
- self.send_to_model(cx);
- cx.notify();
+ if self.focus_active_patch(cx) {
+ return;
}
+
+ self.last_error = None;
+ self.send_to_model(cx);
+ cx.notify();
}
- fn apply_workflow_step(&mut self, range: Range, cx: &mut ViewContext) {
- self.show_workflow_step(range.clone(), cx);
-
- if let Some(workflow_step) = self.workflow_steps.get(&range) {
- if let Some(assist) = workflow_step.assist.as_ref() {
- let assist_ids = assist.assist_ids.clone();
- cx.spawn(|this, mut cx| async move {
- for assist_id in assist_ids {
- let mut receiver = this.update(&mut cx, |_, cx| {
- cx.window_context().defer(move |cx| {
- InlineAssistant::update_global(cx, |assistant, cx| {
- assistant.start_assist(assist_id, cx);
- })
- });
- InlineAssistant::update_global(cx, |assistant, _| {
- assistant.observe_assist(assist_id)
- })
- })?;
- while !receiver.borrow().is_done() {
- let _ = receiver.changed().await;
- }
- }
- anyhow::Ok(())
- })
- .detach_and_log_err(cx);
- }
- }
- }
-
- fn apply_active_workflow_step(&mut self, cx: &mut ViewContext) -> bool {
- let Some((range, step)) = self.active_workflow_step() else {
- return false;
- };
-
- if let Some(assist) = step.assist.as_ref() {
- match assist.status(cx) {
- WorkflowAssistStatus::Pending => {}
- WorkflowAssistStatus::Confirmed => return false,
- WorkflowAssistStatus::Done => self.confirm_workflow_step(range, cx),
- WorkflowAssistStatus::Idle => self.apply_workflow_step(range, cx),
- }
- } else {
- match step.resolution.as_deref() {
- Some(Ok(_)) => self.apply_workflow_step(range, cx),
- Some(Err(_)) => self.resolve_workflow_step(range, cx),
- None => {}
+ fn focus_active_patch(&mut self, cx: &mut ViewContext) -> bool {
+ if let Some((_range, patch)) = self.active_patch() {
+ if let Some(editor) = patch
+ .editor
+ .as_ref()
+ .and_then(|state| state.editor.upgrade())
+ {
+ cx.focus_view(&editor);
+ return true;
}
}
- true
- }
-
- fn resolve_workflow_step(
- &mut self,
- range: Range,
- cx: &mut ViewContext,
- ) {
- self.context
- .update(cx, |context, cx| context.resolve_workflow_step(range, cx));
- }
-
- fn stop_workflow_step(&mut self, range: Range, cx: &mut ViewContext) {
- if let Some(workflow_step) = self.workflow_steps.get(&range) {
- if let Some(assist) = workflow_step.assist.as_ref() {
- let assist_ids = assist.assist_ids.clone();
- cx.window_context().defer(|cx| {
- InlineAssistant::update_global(cx, |assistant, cx| {
- for assist_id in assist_ids {
- assistant.stop_assist(assist_id, cx);
- }
- })
- });
- }
- }
- }
-
- fn undo_workflow_step(&mut self, range: Range, cx: &mut ViewContext) {
- if let Some(workflow_step) = self.workflow_steps.get_mut(&range) {
- if let Some(assist) = workflow_step.assist.take() {
- cx.window_context().defer(|cx| {
- InlineAssistant::update_global(cx, |assistant, cx| {
- for assist_id in assist.assist_ids {
- assistant.undo_assist(assist_id, cx);
- }
- })
- });
- }
- }
- }
-
- fn confirm_workflow_step(
- &mut self,
- range: Range,
- cx: &mut ViewContext,
- ) {
- if let Some(workflow_step) = self.workflow_steps.get(&range) {
- if let Some(assist) = workflow_step.assist.as_ref() {
- let assist_ids = assist.assist_ids.clone();
- cx.window_context().defer(move |cx| {
- InlineAssistant::update_global(cx, |assistant, cx| {
- for assist_id in assist_ids {
- assistant.finish_assist(assist_id, false, cx);
- }
- })
- });
- }
- }
- }
-
- fn reject_workflow_step(&mut self, range: Range, cx: &mut ViewContext) {
- if let Some(workflow_step) = self.workflow_steps.get_mut(&range) {
- if let Some(assist) = workflow_step.assist.take() {
- cx.window_context().defer(move |cx| {
- InlineAssistant::update_global(cx, |assistant, cx| {
- for assist_id in assist.assist_ids {
- assistant.finish_assist(assist_id, true, cx);
- }
- })
- });
- }
- }
+ false
}
fn send_to_model(&mut self, cx: &mut ViewContext) {
@@ -1802,19 +1650,6 @@ impl ContextEditor {
return;
}
- if let Some((range, active_step)) = self.active_workflow_step() {
- match active_step.status(cx) {
- WorkflowStepStatus::Pending => {
- self.stop_workflow_step(range, cx);
- return;
- }
- WorkflowStepStatus::Done => {
- self.reject_workflow_step(range, cx);
- return;
- }
- _ => {}
- }
- }
cx.propagate();
}
@@ -2068,8 +1903,8 @@ impl ContextEditor {
);
});
}
- ContextEvent::WorkflowStepsUpdated { removed, updated } => {
- self.workflow_steps_updated(removed, updated, cx);
+ ContextEvent::PatchesUpdated { removed, updated } => {
+ self.patches_updated(removed, updated, cx);
}
ContextEvent::PendingSlashCommandsUpdated { removed, updated } => {
self.editor.update(cx, |editor, cx| {
@@ -2309,7 +2144,7 @@ impl ContextEditor {
}
}
- fn workflow_steps_updated(
+ fn patches_updated(
&mut self,
removed: &Vec>,
updated: &Vec>,
@@ -2320,218 +2155,133 @@ impl ContextEditor {
let mut removed_block_ids = HashSet::default();
let mut editors_to_close = Vec::new();
for range in removed {
- if let Some(state) = self.workflow_steps.remove(range) {
- editors_to_close.extend(self.hide_workflow_step(range.clone(), cx));
- removed_block_ids.insert(state.header_block_id);
- removed_crease_ids.push(state.header_crease_id);
- removed_block_ids.extend(state.footer_block_id);
- removed_crease_ids.extend(state.footer_crease_id);
+ if let Some(state) = self.patches.remove(range) {
+ editors_to_close.extend(state.editor.and_then(|state| state.editor.upgrade()));
+ removed_block_ids.insert(state.footer_block_id);
+ removed_crease_ids.push(state.crease_id);
}
}
- for range in updated {
- editors_to_close.extend(self.hide_workflow_step(range.clone(), cx));
- }
-
self.editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let multibuffer = &snapshot.buffer_snapshot;
- let (&excerpt_id, _, buffer) = multibuffer.as_singleton().unwrap();
+ let (&excerpt_id, _, _) = multibuffer.as_singleton().unwrap();
+ let mut replaced_blocks = HashMap::default();
for range in updated {
- let Some(step) = self.context.read(cx).workflow_step_for_range(&range, cx) else {
+ let Some(patch) = self.context.read(cx).patch_for_range(&range, cx).cloned() else {
continue;
};
- let resolution = step.resolution.clone();
- let header_start = step.range.start;
- let header_end = if buffer.contains_str_at(step.leading_tags_end, "\n") {
- buffer.anchor_before(step.leading_tags_end.to_offset(&buffer) + 1)
- } else {
- step.leading_tags_end
- };
- let header_range = multibuffer
- .anchor_in_excerpt(excerpt_id, header_start)
- .unwrap()
- ..multibuffer
- .anchor_in_excerpt(excerpt_id, header_end)
- .unwrap();
- let footer_range = step.trailing_tag_start.map(|start| {
- let mut step_range_end = step.range.end.to_offset(&buffer);
- if buffer.contains_str_at(step_range_end, "\n") {
- // Only include the newline if it belongs to the same message.
- let messages = self
- .context
- .read(cx)
- .messages_for_offsets([step_range_end, step_range_end + 1], cx);
- if messages.len() == 1 {
- step_range_end += 1;
- }
+ let path_count = patch.path_count();
+ let patch_start = multibuffer
+ .anchor_in_excerpt(excerpt_id, patch.range.start)
+ .unwrap();
+ let patch_end = multibuffer
+ .anchor_in_excerpt(excerpt_id, patch.range.end)
+ .unwrap();
+ let render_block: RenderBlock = Box::new({
+ let this = this.clone();
+ let patch_range = range.clone();
+ move |cx: &mut BlockContext<'_, '_>| {
+ let max_width = cx.max_width;
+ let gutter_width = cx.gutter_dimensions.full_width();
+ let block_id = cx.block_id;
+ this.update(&mut **cx, |this, cx| {
+ this.render_patch_footer(
+ patch_range.clone(),
+ max_width,
+ gutter_width,
+ block_id,
+ cx,
+ )
+ })
+ .ok()
+ .flatten()
+ .unwrap_or_else(|| Empty.into_any())
}
-
- let end = buffer.anchor_before(step_range_end);
- multibuffer.anchor_in_excerpt(excerpt_id, start).unwrap()
- ..multibuffer.anchor_in_excerpt(excerpt_id, end).unwrap()
});
- let block_ids = editor.insert_blocks(
- [BlockProperties {
- position: header_range.start,
- height: 1,
- style: BlockStyle::Flex,
- render: Box::new({
- let this = this.clone();
- let range = step.range.clone();
- move |cx| {
- let block_id = cx.block_id;
- let max_width = cx.max_width;
- let gutter_width = cx.gutter_dimensions.full_width();
- this.update(&mut **cx, |this, cx| {
- this.render_workflow_step_header(
- range.clone(),
- max_width,
- gutter_width,
- block_id,
- cx,
- )
- })
- .ok()
- .flatten()
- .unwrap_or_else(|| Empty.into_any())
- }
- }),
- disposition: BlockDisposition::Above,
- priority: 0,
- }]
- .into_iter()
- .chain(footer_range.as_ref().map(|footer_range| {
- return BlockProperties {
- position: footer_range.end,
- height: 1,
- style: BlockStyle::Flex,
- render: Box::new({
+ let header_placeholder = FoldPlaceholder {
+ render: {
+ let this = this.clone();
+ let patch_range = range.clone();
+ Arc::new(move |fold_id, _range, cx| {
+ this.update(cx, |this, cx| {
+ this.render_patch_header(patch_range.clone(), fold_id, cx)
+ })
+ .ok()
+ .flatten()
+ .unwrap_or_else(|| Empty.into_any())
+ })
+ },
+ constrain_width: false,
+ merge_adjacent: false,
+ };
+
+ if let Some(state) = self.patches.get_mut(&range) {
+ replaced_blocks.insert(state.footer_block_id, render_block);
+ if let Some(editor_state) = &state.editor {
+ if editor_state.opened_patch != patch {
+ state.update_task = Some({
let this = this.clone();
- let range = step.range.clone();
- move |cx| {
- let max_width = cx.max_width;
- let gutter_width = cx.gutter_dimensions.full_width();
- this.update(&mut **cx, |this, cx| {
- this.render_workflow_step_footer(
- range.clone(),
- max_width,
- gutter_width,
- cx,
- )
- })
- .ok()
- .flatten()
- .unwrap_or_else(|| Empty.into_any())
- }
- }),
+ cx.spawn(|_, cx| async move {
+ Self::update_patch_editor(this.clone(), patch, cx)
+ .await
+ .log_err();
+ })
+ });
+ }
+ }
+ } else {
+ let block_ids = editor.insert_blocks(
+ [BlockProperties {
+ position: patch_start,
+ height: path_count as u32 + 1,
+ style: BlockStyle::Flex,
+ render: render_block,
disposition: BlockDisposition::Below,
priority: 0,
- };
- })),
- None,
- cx,
- );
+ }],
+ None,
+ cx,
+ );
- let header_placeholder = FoldPlaceholder {
- render: Arc::new(move |_, _crease_range, _cx| Empty.into_any()),
- constrain_width: false,
- merge_adjacent: false,
- };
- let footer_placeholder = FoldPlaceholder {
- render: render_fold_icon_button(
- cx.view().downgrade(),
- IconName::Code,
- "Edits".into(),
- ),
- constrain_width: false,
- merge_adjacent: false,
- };
-
- let new_crease_ids = editor.insert_creases(
- [Crease::new(
- header_range.clone(),
- header_placeholder.clone(),
- fold_toggle("step-header"),
- |_, _, _| Empty.into_any_element(),
- )]
- .into_iter()
- .chain(footer_range.clone().map(|footer_range| {
- Crease::new(
- footer_range,
- footer_placeholder.clone(),
- |row, is_folded, fold, cx| {
- if is_folded {
- Empty.into_any_element()
- } else {
- fold_toggle("step-footer")(row, is_folded, fold, cx)
- }
- },
+ let new_crease_ids = editor.insert_creases(
+ [Crease::new(
+ patch_start..patch_end,
+ header_placeholder.clone(),
+ fold_toggle("patch-header"),
|_, _, _| Empty.into_any_element(),
- )
- })),
- cx,
- );
+ )],
+ cx,
+ );
- let state = WorkflowStepViewState {
- header_block_id: block_ids[0],
- header_crease_id: new_crease_ids[0],
- footer_block_id: block_ids.get(1).copied(),
- footer_crease_id: new_crease_ids.get(1).copied(),
- resolution,
- assist: None,
- };
-
- let mut folds_to_insert = [(header_range.clone(), header_placeholder)]
- .into_iter()
- .chain(
- footer_range
- .clone()
- .map(|range| (range, footer_placeholder)),
- )
- .collect::>();
-
- match self.workflow_steps.entry(range.clone()) {
- hash_map::Entry::Vacant(entry) => {
- entry.insert(state);
- }
- hash_map::Entry::Occupied(mut entry) => {
- let entry = entry.get_mut();
- removed_block_ids.insert(entry.header_block_id);
- removed_crease_ids.push(entry.header_crease_id);
- removed_block_ids.extend(entry.footer_block_id);
- removed_crease_ids.extend(entry.footer_crease_id);
- folds_to_insert.retain(|(range, _)| snapshot.intersects_fold(range.start));
- *entry = state;
- }
+ self.patches.insert(
+ range.clone(),
+ PatchViewState {
+ footer_block_id: block_ids[0],
+ crease_id: new_crease_ids[0],
+ editor: None,
+ update_task: None,
+ },
+ );
}
- editor.unfold_ranges(
- [header_range.clone()]
- .into_iter()
- .chain(footer_range.clone()),
- true,
- false,
- cx,
- );
-
- if !folds_to_insert.is_empty() {
- editor.fold_ranges(folds_to_insert, false, cx);
- }
+ editor.unfold_ranges([patch_start..patch_end], true, false, cx);
+ editor.fold_ranges([(patch_start..patch_end, header_placeholder)], false, cx);
}
editor.remove_creases(removed_crease_ids, cx);
editor.remove_blocks(removed_block_ids, None, cx);
+ editor.replace_blocks(replaced_blocks, None, cx);
});
- for (editor, editor_was_open) in editors_to_close {
- self.close_workflow_editor(cx, editor, editor_was_open);
+ for editor in editors_to_close {
+ self.close_patch_editor(editor, cx);
}
- self.update_active_workflow_step(cx);
+ self.update_active_patch(cx);
}
fn insert_slash_command_output_sections(
@@ -2604,87 +2354,75 @@ impl ContextEditor {
}
EditorEvent::SelectionsChanged { .. } => {
self.scroll_position = self.cursor_scroll_position(cx);
- self.update_active_workflow_step(cx);
+ self.update_active_patch(cx);
}
_ => {}
}
cx.emit(event.clone());
}
- fn active_workflow_step(&self) -> Option<(Range, &WorkflowStepViewState)> {
- let step = self.active_workflow_step.as_ref()?;
- Some((step.range.clone(), self.workflow_steps.get(&step.range)?))
+ fn active_patch(&self) -> Option<(Range, &PatchViewState)> {
+ let patch = self.active_patch.as_ref()?;
+ Some((patch.clone(), self.patches.get(&patch)?))
}
- fn update_active_workflow_step(&mut self, cx: &mut ViewContext) {
- let newest_cursor = self.editor.read(cx).selections.newest::(cx).head();
+ fn update_active_patch(&mut self, cx: &mut ViewContext) {
+ let newest_cursor = self.editor.read(cx).selections.newest::(cx).head();
let context = self.context.read(cx);
- let new_step = context
- .workflow_step_containing(newest_cursor, cx)
- .map(|step| ActiveWorkflowStep {
- resolved: step.resolution.is_some(),
- range: step.range.clone(),
- });
+ let new_patch = context.patch_containing(newest_cursor, cx).cloned();
- if new_step.as_ref() != self.active_workflow_step.as_ref() {
- let mut old_editor = None;
- let mut old_editor_was_open = None;
- if let Some(old_step) = self.active_workflow_step.take() {
- (old_editor, old_editor_was_open) =
- self.hide_workflow_step(old_step.range, cx).unzip();
+ if new_patch.as_ref().map(|p| &p.range) == self.active_patch.as_ref() {
+ return;
+ }
+
+ if let Some(old_patch_range) = self.active_patch.take() {
+ if let Some(patch_state) = self.patches.get_mut(&old_patch_range) {
+ if let Some(state) = patch_state.editor.take() {
+ if let Some(editor) = state.editor.upgrade() {
+ self.close_patch_editor(editor, cx);
+ }
+ }
}
+ }
- let mut new_editor = None;
- if let Some(new_step) = new_step {
- new_editor = self.show_workflow_step(new_step.range.clone(), cx);
- self.active_workflow_step = Some(new_step);
- }
+ if let Some(new_patch) = new_patch {
+ self.active_patch = Some(new_patch.range.clone());
- if new_editor != old_editor {
- if let Some((old_editor, old_editor_was_open)) = old_editor.zip(old_editor_was_open)
- {
- self.close_workflow_editor(cx, old_editor, old_editor_was_open)
+ if let Some(patch_state) = self.patches.get_mut(&new_patch.range) {
+ let mut editor = None;
+ if let Some(state) = &patch_state.editor {
+ if let Some(opened_editor) = state.editor.upgrade() {
+ editor = Some(opened_editor);
+ }
+ }
+
+ if let Some(editor) = editor {
+ self.workspace
+ .update(cx, |workspace, cx| {
+ workspace.activate_item(&editor, true, false, cx);
+ })
+ .ok();
+ } else {
+ patch_state.update_task = Some(cx.spawn(move |this, cx| async move {
+ Self::open_patch_editor(this, new_patch, cx).await.log_err();
+ }));
}
}
}
}
- fn hide_workflow_step(
- &mut self,
- step_range: Range,
- cx: &mut ViewContext,
- ) -> Option<(View, bool)> {
- if let Some(step) = self.workflow_steps.get_mut(&step_range) {
- let assist = step.assist.as_ref()?;
- let editor = assist.editor.upgrade()?;
-
- if matches!(step.status(cx), WorkflowStepStatus::Idle) {
- let assist = step.assist.take().unwrap();
- InlineAssistant::update_global(cx, |assistant, cx| {
- for assist_id in assist.assist_ids {
- assistant.finish_assist(assist_id, true, cx)
- }
- });
- return Some((editor, assist.editor_was_open));
- }
- }
-
- None
- }
-
- fn close_workflow_editor(
+ fn close_patch_editor(
&mut self,
+ editor: View,
cx: &mut ViewContext,
- editor: View,
- editor_was_open: bool,
) {
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 !editor_was_open && !editor.read(cx).is_focused(cx) {
+ if !editor.read(cx).focus_handle(cx).is_focused(cx) {
pane.close_item_by_id(item_id, SaveIntent::Skip, cx)
.detach_and_log_err(cx);
}
@@ -2694,190 +2432,94 @@ impl ContextEditor {
.ok();
}
- fn show_workflow_step(
- &mut self,
- step_range: Range,
- cx: &mut ViewContext,
- ) -> Option> {
- let step = self.workflow_steps.get_mut(&step_range)?;
+ async fn open_patch_editor(
+ this: WeakView,
+ patch: AssistantPatch,
+ mut cx: AsyncWindowContext,
+ ) -> Result<()> {
+ let project = this.update(&mut cx, |this, _| this.project.clone())?;
+ let resolved_patch = patch.resolve(project.clone(), &mut cx).await;
- let mut editor_to_return = None;
- let mut scroll_to_assist_id = None;
- match step.status(cx) {
- WorkflowStepStatus::Idle => {
- if let Some(assist) = step.assist.as_ref() {
- scroll_to_assist_id = assist.assist_ids.first().copied();
- } else if let Some(Ok(resolved)) = step.resolution.clone().as_deref() {
- step.assist = Self::open_assists_for_step(
- &resolved,
- &self.project,
- &self.assistant_panel,
- &self.workspace,
- cx,
- );
- editor_to_return = step
- .assist
- .as_ref()
- .and_then(|assist| assist.editor.upgrade());
- }
- }
- WorkflowStepStatus::Pending => {
- if let Some(assist) = step.assist.as_ref() {
- let assistant = InlineAssistant::global(cx);
- scroll_to_assist_id = assist
- .assist_ids
- .iter()
- .copied()
- .find(|assist_id| assistant.assist_status(*assist_id, cx).is_pending());
- }
- }
- WorkflowStepStatus::Done => {
- if let Some(assist) = step.assist.as_ref() {
- scroll_to_assist_id = assist.assist_ids.first().copied();
- }
- }
- _ => {}
- }
-
- if let Some(assist_id) = scroll_to_assist_id {
- if let Some(assist_editor) = step
- .assist
- .as_ref()
- .and_then(|assists| assists.editor.upgrade())
- {
- editor_to_return = Some(assist_editor.clone());
- self.workspace
- .update(cx, |workspace, cx| {
- workspace.activate_item(&assist_editor, false, false, cx);
+ let editor = cx.new_view(|cx| {
+ let editor = ProposedChangesEditor::new(
+ patch.title.clone(),
+ resolved_patch
+ .edit_groups
+ .iter()
+ .map(|(buffer, groups)| ProposedChangeLocation {
+ buffer: buffer.clone(),
+ ranges: groups
+ .iter()
+ .map(|group| group.context_range.clone())
+ .collect(),
})
- .ok();
- InlineAssistant::update_global(cx, |assistant, cx| {
- assistant.scroll_to_assist(assist_id, cx)
+ .collect(),
+ Some(project.clone()),
+ cx,
+ );
+ resolved_patch.apply(&editor, cx);
+ editor
+ })?;
+
+ this.update(&mut cx, |this, cx| {
+ if let Some(patch_state) = this.patches.get_mut(&patch.range) {
+ patch_state.editor = Some(PatchEditorState {
+ editor: editor.downgrade(),
+ opened_patch: patch,
});
+ patch_state.update_task.take();
}
- }
- editor_to_return
- }
-
- fn open_assists_for_step(
- resolved_step: &WorkflowStepResolution,
- project: &Model,
- assistant_panel: &WeakView,
- workspace: &WeakView,
- cx: &mut ViewContext,
- ) -> Option {
- let assistant_panel = assistant_panel.upgrade()?;
- if resolved_step.suggestion_groups.is_empty() {
- return None;
- }
-
- let editor;
- let mut editor_was_open = false;
- let mut suggestion_groups = Vec::new();
- if resolved_step.suggestion_groups.len() == 1
- && resolved_step
- .suggestion_groups
- .values()
- .next()
- .unwrap()
- .len()
- == 1
- {
- // If there's only one buffer and one suggestion group, open it directly
- let (buffer, groups) = resolved_step.suggestion_groups.iter().next().unwrap();
- let group = groups.into_iter().next().unwrap();
- editor = workspace
- .update(cx, |workspace, cx| {
- let active_pane = workspace.active_pane().clone();
- editor_was_open =
- workspace.is_project_item_open::(&active_pane, buffer, cx);
- workspace.open_project_item::(
- active_pane,
- buffer.clone(),
- false,
- false,
- cx,
- )
- })
- .log_err()?;
- let (&excerpt_id, _, _) = editor
- .read(cx)
- .buffer()
- .read(cx)
- .read(cx)
- .as_singleton()
- .unwrap();
-
- // Scroll the editor to the suggested assist
- editor.update(cx, |editor, cx| {
- let multibuffer = editor.buffer().read(cx).snapshot(cx);
- let (&excerpt_id, _, buffer) = multibuffer.as_singleton().unwrap();
- let anchor = if group.context_range.start.to_offset(buffer) == 0 {
- Anchor::min()
- } else {
- multibuffer
- .anchor_in_excerpt(excerpt_id, group.context_range.start)
- .unwrap()
- };
-
- editor.set_scroll_anchor(
- ScrollAnchor {
- offset: gpui::Point::default(),
- anchor,
- },
- cx,
- );
- });
-
- suggestion_groups.push((excerpt_id, group));
- } else {
- // If there are multiple buffers or suggestion groups, create a multibuffer
- let multibuffer = cx.new_model(|cx| {
- let mut multibuffer =
- MultiBuffer::new(Capability::ReadWrite).with_title(resolved_step.title.clone());
- for (buffer, groups) in &resolved_step.suggestion_groups {
- let excerpt_ids = multibuffer.push_excerpts(
- buffer.clone(),
- groups.iter().map(|suggestion_group| ExcerptRange {
- context: suggestion_group.context_range.clone(),
- primary: None,
- }),
- cx,
- );
- suggestion_groups.extend(excerpt_ids.into_iter().zip(groups));
- }
- multibuffer
- });
-
- editor = cx.new_view(|cx| {
- Editor::for_multibuffer(multibuffer, Some(project.clone()), true, cx)
- });
- workspace
+ this.workspace
.update(cx, |workspace, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx)
})
- .log_err()?;
- }
+ .log_err();
+ })?;
- let mut assist_ids = Vec::new();
- for (excerpt_id, suggestion_group) in suggestion_groups {
- for suggestion in &suggestion_group.suggestions {
- assist_ids.extend(suggestion.show(
- &editor,
- excerpt_id,
- workspace,
- &assistant_panel,
- cx,
- ));
+ Ok(())
+ }
+
+ async fn update_patch_editor(
+ this: WeakView,
+ patch: AssistantPatch,
+ mut cx: AsyncWindowContext,
+ ) -> Result<()> {
+ let project = this.update(&mut cx, |this, _| this.project.clone())?;
+ let resolved_patch = patch.resolve(project.clone(), &mut cx).await;
+ this.update(&mut cx, |this, cx| {
+ let patch_state = this.patches.get_mut(&patch.range)?;
+
+ let locations = resolved_patch
+ .edit_groups
+ .iter()
+ .map(|(buffer, groups)| ProposedChangeLocation {
+ buffer: buffer.clone(),
+ ranges: groups
+ .iter()
+ .map(|group| group.context_range.clone())
+ .collect(),
+ })
+ .collect();
+
+ if let Some(state) = &mut patch_state.editor {
+ if let Some(editor) = state.editor.upgrade() {
+ editor.update(cx, |editor, cx| {
+ editor.set_title(patch.title.clone(), cx);
+ editor.reset_locations(locations, cx);
+ resolved_patch.apply(editor, cx);
+ });
+
+ state.opened_patch = patch;
+ } else {
+ patch_state.editor.take();
+ }
}
- }
+ patch_state.update_task.take();
- Some(WorkflowAssist {
- assist_ids,
- editor: editor.downgrade(),
- editor_was_open,
- })
+ Some(())
+ })?;
+ Ok(())
}
fn handle_editor_search_event(
@@ -3782,394 +3424,91 @@ impl ContextEditor {
.unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
}
- fn render_workflow_step_header(
+ fn render_patch_header(
&self,
range: Range,
+ _id: FoldId,
+ cx: &mut ViewContext,
+ ) -> Option {
+ let patch = self.context.read(cx).patch_for_range(&range, cx)?;
+ let theme = cx.theme().clone();
+ Some(
+ h_flex()
+ .px_1()
+ .py_0p5()
+ .border_b_1()
+ .border_color(theme.status().info_border)
+ .gap_1()
+ .child(Icon::new(IconName::Diff).size(IconSize::Small))
+ .child(Label::new(patch.title.clone()).size(LabelSize::Small))
+ .into_any(),
+ )
+ }
+
+ fn render_patch_footer(
+ &mut self,
+ range: Range,
max_width: Pixels,
gutter_width: Pixels,
id: BlockId,
cx: &mut ViewContext,
) -> Option {
- let step_state = self.workflow_steps.get(&range)?;
- let status = step_state.status(cx);
- let this = cx.view().downgrade();
+ let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
+ let (excerpt_id, _buffer_id, _) = snapshot.buffer_snapshot.as_singleton().unwrap();
+ let excerpt_id = *excerpt_id;
+ let anchor = snapshot
+ .buffer_snapshot
+ .anchor_in_excerpt(excerpt_id, range.start)
+ .unwrap();
- let theme = cx.theme().status();
- let is_confirmed = status.is_confirmed();
- let border_color = if is_confirmed {
- theme.ignored_border
- } else {
- theme.info_border
- };
-
- let editor = self.editor.read(cx);
- let focus_handle = editor.focus_handle(cx);
- let snapshot = editor
- .buffer()
- .read(cx)
- .as_singleton()?
- .read(cx)
- .text_snapshot();
- let start_offset = range.start.to_offset(&snapshot);
- let parent_message = self
- .context
- .read(cx)
- .messages_for_offsets([start_offset], cx);
- debug_assert_eq!(parent_message.len(), 1);
- let parent_message = parent_message.first()?;
-
- let step_index = self
- .workflow_steps
- .keys()
- .filter(|workflow_step_range| {
- workflow_step_range
- .start
- .cmp(&parent_message.anchor_range.start, &snapshot)
- .is_ge()
- && workflow_step_range.end.cmp(&range.end, &snapshot).is_le()
- })
- .count();
-
- let step_label = Label::new(format!("Step {step_index}")).size(LabelSize::Small);
-
- let step_label = if is_confirmed {
- h_flex()
- .items_center()
- .gap_2()
- .child(step_label.strikethrough(true).color(Color::Muted))
- .child(
- Icon::new(IconName::Check)
- .size(IconSize::Small)
- .color(Color::Created),
- )
- } else {
- div().child(step_label)
- };
-
- Some(
- v_flex()
- .w(max_width)
- .pl(gutter_width)
- .child(
- h_flex()
- .w_full()
- .h_8()
- .border_b_1()
- .border_color(border_color)
- .items_center()
- .justify_between()
- .gap_2()
- .child(h_flex().justify_start().gap_2().child(step_label))
- .child(h_flex().w_full().justify_end().child(
- Self::render_workflow_step_status(
- status,
- range.clone(),
- focus_handle.clone(),
- this.clone(),
- id,
- ),
- )),
- )
- // todo!("do we wanna keep this?")
- // .children(edit_paths.iter().map(|path| {
- // h_flex()
- // .gap_1()
- // .child(Icon::new(IconName::File))
- // .child(Label::new(path.clone()))
- // }))
- .into_any(),
- )
- }
-
- fn render_workflow_step_footer(
- &self,
- step_range: Range,
- max_width: Pixels,
- gutter_width: Pixels,
- cx: &mut ViewContext,
- ) -> Option {
- let step = self.workflow_steps.get(&step_range)?;
- let current_status = step.status(cx);
- let theme = cx.theme().status();
- let border_color = if current_status.is_confirmed() {
- theme.ignored_border
- } else {
- theme.info_border
- };
- Some(
- v_flex()
- .w(max_width)
- .pt_1()
- .pl(gutter_width)
- .child(h_flex().h(px(1.)).bg(border_color))
- .into_any(),
- )
- }
-
- fn render_workflow_step_status(
- status: WorkflowStepStatus,
- step_range: Range,
- focus_handle: FocusHandle,
- editor: WeakView,
- id: BlockId,
- ) -> AnyElement {
- let id = EntityId::from(id).as_u64();
- fn display_keybind_in_tooltip(
- step_range: &Range,
- editor: &WeakView,
- cx: &mut WindowContext<'_>,
- ) -> bool {
- editor
- .update(cx, |this, _| {
- this.active_workflow_step
- .as_ref()
- .map(|step| &step.range == step_range)
- })
- .ok()
- .flatten()
- .unwrap_or_default()
+ if !snapshot.intersects_fold(anchor) {
+ return None;
}
- match status {
- WorkflowStepStatus::Error(error) => {
- let error = error.to_string();
- h_flex()
- .gap_2()
- .child(
- div()
- .id("step-resolution-failure")
- .child(
- Label::new("Step Resolution Failed")
- .size(LabelSize::Small)
- .color(Color::Error),
- )
- .tooltip(move |cx| Tooltip::text(error.clone(), cx)),
- )
- .child(
- Button::new(("transform", id), "Retry")
- .icon(IconName::Update)
- .icon_position(IconPosition::Start)
- .icon_size(IconSize::Small)
- .label_size(LabelSize::Small)
- .on_click({
- let editor = editor.clone();
- let step_range = step_range.clone();
- move |_, cx| {
- editor
- .update(cx, |this, cx| {
- this.resolve_workflow_step(step_range.clone(), cx)
- })
- .ok();
- }
- }),
- )
- .into_any()
- }
- WorkflowStepStatus::Idle | WorkflowStepStatus::Resolving { .. } => {
- Button::new(("transform", id), "Transform")
- .icon(IconName::SparkleAlt)
- .icon_position(IconPosition::Start)
- .icon_size(IconSize::Small)
- .label_size(LabelSize::Small)
- .style(ButtonStyle::Tinted(TintColor::Accent))
- .tooltip({
- let step_range = step_range.clone();
- let editor = editor.clone();
- move |cx| {
- cx.new_view(|cx| {
- let tooltip = Tooltip::new("Transform");
- if display_keybind_in_tooltip(&step_range, &editor, cx) {
- tooltip.key_binding(KeyBinding::for_action_in(
- &Assist,
- &focus_handle,
- cx,
- ))
- } else {
- tooltip
- }
- })
- .into()
- }
- })
- .on_click({
- let editor = editor.clone();
- let step_range = step_range.clone();
- let is_idle = matches!(status, WorkflowStepStatus::Idle);
- move |_, cx| {
- if is_idle {
- editor
- .update(cx, |this, cx| {
- this.apply_workflow_step(step_range.clone(), cx)
- })
- .ok();
- }
- }
- })
- .map(|this| {
- if let WorkflowStepStatus::Resolving = &status {
- this.with_animation(
- ("resolving-suggestion-animation", id),
+ let patch = self.context.read(cx).patch_for_range(&range, cx)?;
+ let paths = patch
+ .paths()
+ .map(|p| SharedString::from(p.to_string()))
+ .collect::>();
+
+ Some(
+ v_flex()
+ .id(id)
+ .pl(gutter_width)
+ .w(max_width)
+ .py_2()
+ .cursor(CursorStyle::PointingHand)
+ .on_click(cx.listener(move |this, _, cx| {
+ this.editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |selections| {
+ selections.select_ranges(vec![anchor..anchor]);
+ });
+ });
+ this.focus_active_patch(cx);
+ }))
+ .children(paths.into_iter().map(|path| {
+ h_flex()
+ .pl_1()
+ .gap_1()
+ .child(Icon::new(IconName::File).size(IconSize::Small))
+ .child(Label::new(path).size(LabelSize::Small))
+ }))
+ .when(patch.status == AssistantPatchStatus::Pending, |div| {
+ div.child(
+ Label::new("Generating")
+ .color(Color::Muted)
+ .size(LabelSize::Small)
+ .with_animation(
+ "pulsating-label",
Animation::new(Duration::from_secs(2))
.repeat()
- .with_easing(pulsating_between(0.4, 0.8)),
+ .with_easing(pulsating_between(0.4, 1.)),
|label, delta| label.alpha(delta),
- )
- .into_any_element()
- } else {
- this.into_any_element()
- }
- })
- }
- WorkflowStepStatus::Pending => h_flex()
- .items_center()
- .gap_2()
- .child(
- Label::new("Applying...")
- .size(LabelSize::Small)
- .with_animation(
- ("applying-step-transformation-label", id),
- Animation::new(Duration::from_secs(2))
- .repeat()
- .with_easing(pulsating_between(0.4, 0.8)),
- |label, delta| label.alpha(delta),
- ),
- )
- .child(
- IconButton::new(("stop-transformation", id), IconName::Stop)
- .icon_size(IconSize::Small)
- .icon_color(Color::Error)
- .style(ButtonStyle::Subtle)
- .tooltip({
- let step_range = step_range.clone();
- let editor = editor.clone();
- move |cx| {
- cx.new_view(|cx| {
- let tooltip = Tooltip::new("Stop Transformation");
- if display_keybind_in_tooltip(&step_range, &editor, cx) {
- tooltip.key_binding(KeyBinding::for_action_in(
- &editor::actions::Cancel,
- &focus_handle,
- cx,
- ))
- } else {
- tooltip
- }
- })
- .into()
- }
- })
- .on_click({
- let editor = editor.clone();
- let step_range = step_range.clone();
- move |_, cx| {
- editor
- .update(cx, |this, cx| {
- this.stop_workflow_step(step_range.clone(), cx)
- })
- .ok();
- }
- }),
- )
- .into_any_element(),
- WorkflowStepStatus::Done => h_flex()
- .gap_1()
- .child(
- IconButton::new(("stop-transformation", id), IconName::Close)
- .icon_size(IconSize::Small)
- .style(ButtonStyle::Tinted(TintColor::Negative))
- .tooltip({
- let focus_handle = focus_handle.clone();
- let editor = editor.clone();
- let step_range = step_range.clone();
- move |cx| {
- cx.new_view(|cx| {
- let tooltip = Tooltip::new("Reject Transformation");
- if display_keybind_in_tooltip(&step_range, &editor, cx) {
- tooltip.key_binding(KeyBinding::for_action_in(
- &editor::actions::Cancel,
- &focus_handle,
- cx,
- ))
- } else {
- tooltip
- }
- })
- .into()
- }
- })
- .on_click({
- let editor = editor.clone();
- let step_range = step_range.clone();
- move |_, cx| {
- editor
- .update(cx, |this, cx| {
- this.reject_workflow_step(step_range.clone(), cx);
- })
- .ok();
- }
- }),
- )
- .child(
- Button::new(("confirm-workflow-step", id), "Accept")
- .icon(IconName::Check)
- .icon_position(IconPosition::Start)
- .icon_size(IconSize::Small)
- .label_size(LabelSize::Small)
- .style(ButtonStyle::Tinted(TintColor::Positive))
- .tooltip({
- let editor = editor.clone();
- let step_range = step_range.clone();
- move |cx| {
- cx.new_view(|cx| {
- let tooltip = Tooltip::new("Accept Transformation");
- if display_keybind_in_tooltip(&step_range, &editor, cx) {
- tooltip.key_binding(KeyBinding::for_action_in(
- &Assist,
- &focus_handle,
- cx,
- ))
- } else {
- tooltip
- }
- })
- .into()
- }
- })
- .on_click({
- let editor = editor.clone();
- let step_range = step_range.clone();
- move |_, cx| {
- editor
- .update(cx, |this, cx| {
- this.confirm_workflow_step(step_range.clone(), cx);
- })
- .ok();
- }
- }),
- )
- .into_any_element(),
- WorkflowStepStatus::Confirmed => h_flex()
- .child(
- Button::new(("revert-workflow-step", id), "Undo")
- .style(ButtonStyle::Filled)
- .icon(Some(IconName::Undo))
- .icon_position(IconPosition::Start)
- .icon_size(IconSize::Small)
- .label_size(LabelSize::Small)
- .on_click({
- let editor = editor.clone();
- let step_range = step_range.clone();
- move |_, cx| {
- editor
- .update(cx, |this, cx| {
- this.undo_workflow_step(step_range.clone(), cx);
- })
- .ok();
- }
- }),
- )
- .into_any_element(),
- }
+ ),
+ )
+ })
+ .into_any(),
+ )
}
fn render_notice(&self, cx: &mut ViewContext) -> Option {
@@ -4259,17 +3598,6 @@ impl ContextEditor {
fn render_send_button(&self, cx: &mut ViewContext) -> impl IntoElement {
let focus_handle = self.focus_handle(cx).clone();
- let button_text = match self.active_workflow_step() {
- Some((_, step)) => match step.status(cx) {
- WorkflowStepStatus::Error(_) => "Retry Step Resolution",
- WorkflowStepStatus::Resolving => "Transform",
- WorkflowStepStatus::Idle => "Transform",
- WorkflowStepStatus::Pending => "Applying...",
- WorkflowStepStatus::Done => "Accept",
- WorkflowStepStatus::Confirmed => "Send",
- },
- None => "Send",
- };
let (style, tooltip) = match token_state(&self.context, cx) {
Some(TokenState::NoTokensLeft { .. }) => (
@@ -4309,7 +3637,7 @@ impl ContextEditor {
button.tooltip(move |_| tooltip.clone())
})
.layer(ElevationIndex::ModalSurface)
- .child(Label::new(button_text))
+ .child(Label::new("Send"))
.children(
KeyBinding::for_action_in(&Assist, &focus_handle, cx)
.map(|binding| binding.into_any_element()),
@@ -5226,33 +4554,6 @@ pub enum WorkflowAssistStatus {
Idle,
}
-impl WorkflowAssist {
- pub fn status(&self, cx: &AppContext) -> WorkflowAssistStatus {
- let assistant = InlineAssistant::global(cx);
- if self
- .assist_ids
- .iter()
- .any(|assist_id| assistant.assist_status(*assist_id, cx).is_pending())
- {
- WorkflowAssistStatus::Pending
- } else if self
- .assist_ids
- .iter()
- .all(|assist_id| assistant.assist_status(*assist_id, cx).is_confirmed())
- {
- WorkflowAssistStatus::Confirmed
- } else if self
- .assist_ids
- .iter()
- .all(|assist_id| assistant.assist_status(*assist_id, cx).is_done())
- {
- WorkflowAssistStatus::Done
- } else {
- WorkflowAssistStatus::Idle
- }
- }
-}
-
impl Render for ContextHistory {
fn render(&mut self, _: &mut ViewContext) -> impl IntoElement {
div().size_full().child(self.picker.clone())
diff --git a/crates/assistant/src/assistant_settings.rs b/crates/assistant/src/assistant_settings.rs
index 5aa379bae3..2bab6a9624 100644
--- a/crates/assistant/src/assistant_settings.rs
+++ b/crates/assistant/src/assistant_settings.rs
@@ -2,6 +2,7 @@ use std::sync::Arc;
use ::open_ai::Model as OpenAiModel;
use anthropic::Model as AnthropicModel;
+use feature_flags::FeatureFlagAppExt;
use fs::Fs;
use gpui::{AppContext, Pixels};
use language_model::provider::open_ai;
@@ -61,6 +62,13 @@ pub struct AssistantSettings {
pub default_model: LanguageModelSelection,
pub inline_alternatives: Vec,
pub using_outdated_settings_version: bool,
+ pub enable_experimental_live_diffs: bool,
+}
+
+impl AssistantSettings {
+ pub fn are_live_diffs_enabled(&self, cx: &AppContext) -> bool {
+ cx.is_staff() || self.enable_experimental_live_diffs
+ }
}
/// Assistant panel settings
@@ -238,6 +246,7 @@ impl AssistantSettingsContent {
}
}),
inline_alternatives: None,
+ enable_experimental_live_diffs: None,
},
VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
},
@@ -257,6 +266,7 @@ impl AssistantSettingsContent {
.to_string(),
}),
inline_alternatives: None,
+ enable_experimental_live_diffs: None,
},
}
}
@@ -373,6 +383,7 @@ impl Default for VersionedAssistantSettingsContent {
default_height: None,
default_model: None,
inline_alternatives: None,
+ enable_experimental_live_diffs: None,
})
}
}
@@ -403,6 +414,10 @@ pub struct AssistantSettingsContentV2 {
default_model: Option,
/// Additional models with which to generate alternatives when performing inline assists.
inline_alternatives: Option>,
+ /// Enable experimental live diffs in the assistant panel.
+ ///
+ /// Default: false
+ enable_experimental_live_diffs: Option,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
@@ -525,7 +540,10 @@ impl Settings for AssistantSettings {
);
merge(&mut settings.default_model, value.default_model);
merge(&mut settings.inline_alternatives, value.inline_alternatives);
- // merge(&mut settings.infer_context, value.infer_context); TODO re-enable this once we ship context inference
+ merge(
+ &mut settings.enable_experimental_live_diffs,
+ value.enable_experimental_live_diffs,
+ );
}
Ok(settings)
@@ -584,6 +602,7 @@ mod tests {
dock: None,
default_width: None,
default_height: None,
+ enable_experimental_live_diffs: None,
}),
)
},
diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs
index 610652a371..2818411d0d 100644
--- a/crates/assistant/src/context.rs
+++ b/crates/assistant/src/context.rs
@@ -2,8 +2,8 @@
mod context_tests;
use crate::{
- prompts::PromptBuilder, slash_command::SlashCommandLine, MessageId, MessageStatus,
- WorkflowStep, WorkflowStepEdit, WorkflowStepResolution, WorkflowSuggestionGroup,
+ prompts::PromptBuilder, slash_command::SlashCommandLine, AssistantEdit, AssistantPatch,
+ AssistantPatchStatus, MessageId, MessageStatus,
};
use anyhow::{anyhow, Context as _, Result};
use assistant_slash_command::{
@@ -15,13 +15,10 @@ use clock::ReplicaId;
use collections::{HashMap, HashSet};
use feature_flags::{FeatureFlag, FeatureFlagAppExt};
use fs::{Fs, RemoveOptions};
-use futures::{
- future::{self, Shared},
- FutureExt, StreamExt,
-};
+use futures::{future::Shared, FutureExt, StreamExt};
use gpui::{
- AppContext, AsyncAppContext, Context as _, EventEmitter, Model, ModelContext, RenderImage,
- SharedString, Subscription, Task,
+ AppContext, Context as _, EventEmitter, Model, ModelContext, RenderImage, SharedString,
+ Subscription, Task,
};
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
@@ -38,7 +35,7 @@ use project::Project;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use std::{
- cmp::{self, max, Ordering},
+ cmp::{max, Ordering},
fmt::Debug,
iter, mem,
ops::Range,
@@ -300,7 +297,7 @@ pub enum ContextEvent {
MessagesEdited,
SummaryChanged,
StreamedCompletion,
- WorkflowStepsUpdated {
+ PatchesUpdated {
removed: Vec>,
updated: Vec>,
},
@@ -454,13 +451,14 @@ pub struct XmlTag {
#[derive(Copy, Clone, Debug, strum::EnumString, PartialEq, Eq, strum::AsRefStr)]
#[strum(serialize_all = "snake_case")]
pub enum XmlTagKind {
- Step,
+ Patch,
+ Title,
Edit,
Path,
- Search,
- Within,
- Operation,
Description,
+ OldText,
+ NewText,
+ Operation,
}
pub struct Context {
@@ -490,7 +488,7 @@ pub struct Context {
_subscriptions: Vec,
telemetry: Option>,
language_registry: Arc,
- workflow_steps: Vec,
+ patches: Vec,
xml_tags: Vec,
project: Option>,
prompt_builder: Arc,
@@ -506,7 +504,7 @@ impl ContextAnnotation for PendingSlashCommand {
}
}
-impl ContextAnnotation for WorkflowStep {
+impl ContextAnnotation for AssistantPatch {
fn range(&self) -> &Range {
&self.range
}
@@ -591,7 +589,7 @@ impl Context {
telemetry,
project,
language_registry,
- workflow_steps: Vec::new(),
+ patches: Vec::new(),
xml_tags: Vec::new(),
prompt_builder,
};
@@ -929,48 +927,49 @@ impl Context {
self.summary.as_ref()
}
- pub(crate) fn workflow_step_containing(
+ pub(crate) fn patch_containing(
&self,
- offset: usize,
+ position: Point,
cx: &AppContext,
- ) -> Option<&WorkflowStep> {
+ ) -> Option<&AssistantPatch> {
let buffer = self.buffer.read(cx);
- let index = self
- .workflow_steps
- .binary_search_by(|step| {
- let step_range = step.range.to_offset(&buffer);
- if offset < step_range.start {
- Ordering::Greater
- } else if offset > step_range.end {
- Ordering::Less
- } else {
- Ordering::Equal
- }
- })
- .ok()?;
- Some(&self.workflow_steps[index])
+ let index = self.patches.binary_search_by(|patch| {
+ let patch_range = patch.range.to_point(&buffer);
+ if position < patch_range.start {
+ Ordering::Greater
+ } else if position > patch_range.end {
+ Ordering::Less
+ } else {
+ Ordering::Equal
+ }
+ });
+ if let Ok(ix) = index {
+ Some(&self.patches[ix])
+ } else {
+ None
+ }
}
- pub fn workflow_step_ranges(&self) -> impl Iterator- > + '_ {
- self.workflow_steps.iter().map(|step| step.range.clone())
+ pub fn patch_ranges(&self) -> impl Iterator
- > + '_ {
+ self.patches.iter().map(|patch| patch.range.clone())
}
- pub(crate) fn workflow_step_for_range(
+ pub(crate) fn patch_for_range(
&self,
range: &Range,
cx: &AppContext,
- ) -> Option<&WorkflowStep> {
+ ) -> Option<&AssistantPatch> {
let buffer = self.buffer.read(cx);
- let index = self.workflow_step_index_for_range(range, buffer).ok()?;
- Some(&self.workflow_steps[index])
+ let index = self.patch_index_for_range(range, buffer).ok()?;
+ Some(&self.patches[index])
}
- fn workflow_step_index_for_range(
+ fn patch_index_for_range(
&self,
tagged_range: &Range,
buffer: &text::BufferSnapshot,
) -> Result {
- self.workflow_steps
+ self.patches
.binary_search_by(|probe| probe.range.cmp(&tagged_range, buffer))
}
@@ -1018,8 +1017,6 @@ impl Context {
language::BufferEvent::Edited => {
self.count_remaining_tokens(cx);
self.reparse(cx);
- // Use `inclusive = true` to invalidate a step when an edit occurs
- // at the start/end of a parsed step.
cx.emit(ContextEvent::MessagesEdited);
}
_ => {}
@@ -1248,8 +1245,8 @@ impl Context {
let mut removed_slash_command_ranges = Vec::new();
let mut updated_slash_commands = Vec::new();
- let mut removed_steps = Vec::new();
- let mut updated_steps = Vec::new();
+ let mut removed_patches = Vec::new();
+ let mut updated_patches = Vec::new();
while let Some(mut row_range) = row_ranges.next() {
while let Some(next_row_range) = row_ranges.peek() {
if row_range.end >= next_row_range.start {
@@ -1273,11 +1270,11 @@ impl Context {
&mut removed_slash_command_ranges,
cx,
);
- self.reparse_workflow_steps_in_range(
+ self.reparse_patches_in_range(
start..end,
&buffer,
- &mut updated_steps,
- &mut removed_steps,
+ &mut updated_patches,
+ &mut removed_patches,
cx,
);
}
@@ -1289,10 +1286,10 @@ impl Context {
});
}
- if !updated_steps.is_empty() || !removed_steps.is_empty() {
- cx.emit(ContextEvent::WorkflowStepsUpdated {
- removed: removed_steps,
- updated: updated_steps,
+ if !updated_patches.is_empty() || !removed_patches.is_empty() {
+ cx.emit(ContextEvent::PatchesUpdated {
+ removed: removed_patches,
+ updated: updated_patches,
});
}
}
@@ -1354,7 +1351,7 @@ impl Context {
removed.extend(removed_commands.map(|command| command.source_range));
}
- fn reparse_workflow_steps_in_range(
+ fn reparse_patches_in_range(
&mut self,
range: Range,
buffer: &BufferSnapshot,
@@ -1369,41 +1366,32 @@ impl Context {
self.xml_tags
.splice(intersecting_tags_range.clone(), new_tags);
- // Find which steps intersect the changed range.
- let intersecting_steps_range =
- self.indices_intersecting_buffer_range(&self.workflow_steps, range.clone(), cx);
+ // Find which patches intersect the changed range.
+ let intersecting_patches_range =
+ self.indices_intersecting_buffer_range(&self.patches, range.clone(), cx);
- // Reparse all tags after the last unchanged step before the change.
+ // Reparse all tags after the last unchanged patch before the change.
let mut tags_start_ix = 0;
- if let Some(preceding_unchanged_step) =
- self.workflow_steps[..intersecting_steps_range.start].last()
+ if let Some(preceding_unchanged_patch) =
+ self.patches[..intersecting_patches_range.start].last()
{
tags_start_ix = match self.xml_tags.binary_search_by(|tag| {
tag.range
.start
- .cmp(&preceding_unchanged_step.range.end, buffer)
+ .cmp(&preceding_unchanged_patch.range.end, buffer)
.then(Ordering::Less)
}) {
Ok(ix) | Err(ix) => ix,
};
}
- // Rebuild the edit suggestions in the range.
- let mut new_steps = self.parse_steps(tags_start_ix, range.end, buffer);
-
- if let Some(project) = self.project() {
- for step in &mut new_steps {
- Self::resolve_workflow_step_internal(step, &project, cx);
- }
- }
-
- updated.extend(new_steps.iter().map(|step| step.range.clone()));
- let removed_steps = self
- .workflow_steps
- .splice(intersecting_steps_range, new_steps);
+ // Rebuild the patches in the range.
+ let new_patches = self.parse_patches(tags_start_ix, range.end, buffer, cx);
+ updated.extend(new_patches.iter().map(|patch| patch.range.clone()));
+ let removed_patches = self.patches.splice(intersecting_patches_range, new_patches);
removed.extend(
- removed_steps
- .map(|step| step.range)
+ removed_patches
+ .map(|patch| patch.range)
.filter(|range| !updated.contains(&range)),
);
}
@@ -1464,60 +1452,95 @@ impl Context {
tags
}
- fn parse_steps(
+ fn parse_patches(
&mut self,
tags_start_ix: usize,
buffer_end: text::Anchor,
buffer: &BufferSnapshot,
- ) -> Vec {
- let mut new_steps = Vec::new();
- let mut pending_step = None;
- let mut edit_step_depth = 0;
+ cx: &AppContext,
+ ) -> Vec {
+ let mut new_patches = Vec::new();
+ let mut pending_patch = None;
+ let mut patch_tag_depth = 0;
let mut tags = self.xml_tags[tags_start_ix..].iter().peekable();
'tags: while let Some(tag) = tags.next() {
- if tag.range.start.cmp(&buffer_end, buffer).is_gt() && edit_step_depth == 0 {
+ if tag.range.start.cmp(&buffer_end, buffer).is_gt() && patch_tag_depth == 0 {
break;
}
- if tag.kind == XmlTagKind::Step && tag.is_open_tag {
- edit_step_depth += 1;
- let edit_start = tag.range.start;
- let mut edits = Vec::new();
- let mut step = WorkflowStep {
- range: edit_start..edit_start,
- leading_tags_end: tag.range.end,
- trailing_tag_start: None,
+ if tag.kind == XmlTagKind::Patch && tag.is_open_tag {
+ patch_tag_depth += 1;
+ let patch_start = tag.range.start;
+ let mut edits = Vec::>::new();
+ let mut patch = AssistantPatch {
+ range: patch_start..patch_start,
+ title: String::new().into(),
edits: Default::default(),
- resolution: None,
- resolution_task: None,
+ status: crate::AssistantPatchStatus::Pending,
};
while let Some(tag) = tags.next() {
- step.trailing_tag_start.get_or_insert(tag.range.start);
+ if tag.kind == XmlTagKind::Patch && !tag.is_open_tag {
+ patch_tag_depth -= 1;
+ if patch_tag_depth == 0 {
+ patch.range.end = tag.range.end;
- if tag.kind == XmlTagKind::Step && !tag.is_open_tag {
- // step.trailing_tag_start = Some(tag.range.start);
- edit_step_depth -= 1;
- if edit_step_depth == 0 {
- step.range.end = tag.range.end;
- step.edits = edits.into();
- new_steps.push(step);
+ // Include the line immediately after this tag if it's empty.
+ let patch_end_offset = patch.range.end.to_offset(buffer);
+ let mut patch_end_chars = buffer.chars_at(patch_end_offset);
+ if patch_end_chars.next() == Some('\n')
+ && patch_end_chars.next().map_or(true, |ch| ch == '\n')
+ {
+ let messages = self.messages_for_offsets(
+ [patch_end_offset, patch_end_offset + 1],
+ cx,
+ );
+ if messages.len() == 1 {
+ patch.range.end = buffer.anchor_before(patch_end_offset + 1);
+ }
+ }
+
+ edits.sort_unstable_by(|a, b| {
+ if let (Ok(a), Ok(b)) = (a, b) {
+ a.path.cmp(&b.path)
+ } else {
+ Ordering::Equal
+ }
+ });
+ patch.edits = edits.into();
+ patch.status = AssistantPatchStatus::Ready;
+ new_patches.push(patch);
continue 'tags;
}
}
+ if tag.kind == XmlTagKind::Title && tag.is_open_tag {
+ let content_start = tag.range.end;
+ while let Some(tag) = tags.next() {
+ if tag.kind == XmlTagKind::Title && !tag.is_open_tag {
+ let content_end = tag.range.start;
+ patch.title =
+ trimmed_text_in_range(buffer, content_start..content_end)
+ .into();
+ break;
+ }
+ }
+ }
+
if tag.kind == XmlTagKind::Edit && tag.is_open_tag {
let mut path = None;
- let mut search = None;
+ let mut old_text = None;
+ let mut new_text = None;
let mut operation = None;
let mut description = None;
while let Some(tag) = tags.next() {
if tag.kind == XmlTagKind::Edit && !tag.is_open_tag {
- edits.push(WorkflowStepEdit::new(
+ edits.push(AssistantEdit::new(
path,
operation,
- search,
+ old_text,
+ new_text,
description,
));
break;
@@ -1526,7 +1549,8 @@ impl Context {
if tag.is_open_tag
&& [
XmlTagKind::Path,
- XmlTagKind::Search,
+ XmlTagKind::OldText,
+ XmlTagKind::NewText,
XmlTagKind::Operation,
XmlTagKind::Description,
]
@@ -1538,15 +1562,18 @@ impl Context {
if tag.kind == kind && !tag.is_open_tag {
let tag = tags.next().unwrap();
let content_end = tag.range.start;
- let mut content = buffer
- .text_for_range(content_start..content_end)
- .collect::();
- content.truncate(content.trim_end().len());
+ let content = trimmed_text_in_range(
+ buffer,
+ content_start..content_end,
+ );
match kind {
XmlTagKind::Path => path = Some(content),
XmlTagKind::Operation => operation = Some(content),
- XmlTagKind::Search => {
- search = Some(content).filter(|s| !s.is_empty())
+ XmlTagKind::OldText => {
+ old_text = Some(content).filter(|s| !s.is_empty())
+ }
+ XmlTagKind::NewText => {
+ new_text = Some(content).filter(|s| !s.is_empty())
}
XmlTagKind::Description => {
description =
@@ -1561,162 +1588,28 @@ impl Context {
}
}
- pending_step = Some(step);
+ patch.edits = edits.into();
+ pending_patch = Some(patch);
}
}
- if let Some(mut pending_step) = pending_step {
- pending_step.range.end = text::Anchor::MAX;
- new_steps.push(pending_step);
- }
-
- new_steps
- }
-
- pub fn resolve_workflow_step(
- &mut self,
- tagged_range: Range,
- cx: &mut ModelContext,
- ) -> Option<()> {
- let index = self
- .workflow_step_index_for_range(&tagged_range, self.buffer.read(cx))
- .ok()?;
- let step = &mut self.workflow_steps[index];
- let project = self.project.as_ref()?;
- step.resolution.take();
- Self::resolve_workflow_step_internal(step, project, cx);
- None
- }
-
- fn resolve_workflow_step_internal(
- step: &mut WorkflowStep,
- project: &Model,
- cx: &mut ModelContext<'_, Context>,
- ) {
- step.resolution_task = Some(cx.spawn({
- let range = step.range.clone();
- let edits = step.edits.clone();
- let project = project.clone();
- |this, mut cx| async move {
- let suggestion_groups =
- Self::compute_step_resolution(project, edits, &mut cx).await;
-
- this.update(&mut cx, |this, cx| {
- let buffer = this.buffer.read(cx).text_snapshot();
- let ix = this.workflow_step_index_for_range(&range, &buffer).ok();
- if let Some(ix) = ix {
- let step = &mut this.workflow_steps[ix];
-
- let resolution = suggestion_groups.map(|suggestion_groups| {
- let mut title = String::new();
- for mut chunk in buffer.text_for_range(
- step.leading_tags_end
- ..step.trailing_tag_start.unwrap_or(step.range.end),
- ) {
- if title.is_empty() {
- chunk = chunk.trim_start();
- }
- if let Some((prefix, _)) = chunk.split_once('\n') {
- title.push_str(prefix);
- break;
- } else {
- title.push_str(chunk);
- }
- }
-
- WorkflowStepResolution {
- title,
- suggestion_groups,
- }
- });
-
- step.resolution = Some(Arc::new(resolution));
- cx.emit(ContextEvent::WorkflowStepsUpdated {
- removed: vec![],
- updated: vec![range],
- })
- }
- })
- .ok();
- }
- }));
- }
-
- async fn compute_step_resolution(
- project: Model,
- edits: Arc<[Result]>,
- cx: &mut AsyncAppContext,
- ) -> Result, Vec>> {
- let mut suggestion_tasks = Vec::new();
- for edit in edits.iter() {
- let edit = edit.as_ref().map_err(|e| anyhow!("{e}"))?;
- suggestion_tasks.push(edit.resolve(project.clone(), cx.clone()));
- }
-
- // Expand the context ranges of each suggestion and group suggestions with overlapping context ranges.
- let suggestions = future::try_join_all(suggestion_tasks).await?;
-
- 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::::new();
- let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
- // Sort suggestions by their range so that earlier, larger ranges come first
- suggestions.sort_by(|a, b| a.range().cmp(&b.range(), &snapshot));
-
- // Merge overlapping suggestions
- suggestions.dedup_by(|a, b| b.try_merge(a, &snapshot));
-
- // Create context ranges for each suggestion
- for suggestion in suggestions {
- let context_range = {
- let suggestion_point_range = suggestion.range().to_point(&snapshot);
- let start_row = suggestion_point_range.start.row.saturating_sub(5);
- let end_row =
- cmp::min(suggestion_point_range.end.row + 5, snapshot.max_point().row);
- let start = snapshot.anchor_before(Point::new(start_row, 0));
- let end =
- snapshot.anchor_after(Point::new(end_row, snapshot.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, &snapshot)
- .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(WorkflowSuggestionGroup {
- context_range,
- suggestions: vec![suggestion],
- });
- }
+ if let Some(mut pending_patch) = pending_patch {
+ let patch_start = pending_patch.range.start.to_offset(buffer);
+ if let Some(message) = self.message_for_offset(patch_start, cx) {
+ if message.anchor_range.end == text::Anchor::MAX {
+ pending_patch.range.end = text::Anchor::MAX;
} else {
- // Create the first group
- suggestion_groups.push(WorkflowSuggestionGroup {
- context_range,
- suggestions: vec![suggestion],
- });
+ let message_end = buffer.anchor_after(message.offset_range.end - 1);
+ pending_patch.range.end = message_end;
}
+ } else {
+ pending_patch.range.end = text::Anchor::MAX;
}
- suggestion_groups_by_buffer.insert(buffer, suggestion_groups);
+ new_patches.push(pending_patch);
}
- Ok(suggestion_groups_by_buffer)
+ new_patches
}
pub fn pending_command_for_position(
@@ -2315,11 +2208,11 @@ impl Context {
let mut updated = Vec::new();
let mut removed = Vec::new();
for range in ranges {
- self.reparse_workflow_steps_in_range(range, &buffer, &mut updated, &mut removed, cx);
+ self.reparse_patches_in_range(range, &buffer, &mut updated, &mut removed, cx);
}
if !updated.is_empty() || !removed.is_empty() {
- cx.emit(ContextEvent::WorkflowStepsUpdated { removed, updated })
+ cx.emit(ContextEvent::PatchesUpdated { removed, updated })
}
}
@@ -2825,6 +2718,24 @@ impl Context {
}
}
+fn trimmed_text_in_range(buffer: &BufferSnapshot, range: Range) -> String {
+ let mut is_start = true;
+ let mut content = buffer
+ .text_for_range(range)
+ .map(|mut chunk| {
+ if is_start {
+ chunk = chunk.trim_start_matches('\n');
+ if !chunk.is_empty() {
+ is_start = false;
+ }
+ }
+ chunk
+ })
+ .collect::();
+ content.truncate(content.trim_end().len());
+ content
+}
+
#[derive(Debug, Default)]
pub struct ContextVersion {
context: clock::Global,
diff --git a/crates/assistant/src/context/context_tests.rs b/crates/assistant/src/context/context_tests.rs
index 2d6a2894c9..a11cfc375d 100644
--- a/crates/assistant/src/context/context_tests.rs
+++ b/crates/assistant/src/context/context_tests.rs
@@ -1,8 +1,7 @@
-use super::{MessageCacheMetadata, WorkflowStepEdit};
+use super::{AssistantEdit, MessageCacheMetadata};
use crate::{
- assistant_panel, prompt_library, slash_command::file_command, CacheStatus, Context,
- ContextEvent, ContextId, ContextOperation, MessageId, MessageStatus, PromptBuilder,
- WorkflowStepEditKind,
+ assistant_panel, prompt_library, slash_command::file_command, AssistantEditKind, CacheStatus,
+ Context, ContextEvent, ContextId, ContextOperation, MessageId, MessageStatus, PromptBuilder,
};
use anyhow::Result;
use assistant_slash_command::{
@@ -15,6 +14,7 @@ use gpui::{AppContext, Model, SharedString, Task, TestAppContext, WeakView};
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
use parking_lot::Mutex;
+use pretty_assertions::assert_eq;
use project::Project;
use rand::prelude::*;
use serde_json::json;
@@ -478,7 +478,15 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
cx.update(prompt_library::init);
- let settings_store = cx.update(SettingsStore::test);
+ let mut settings_store = cx.update(SettingsStore::test);
+ cx.update(|cx| {
+ settings_store
+ .set_user_settings(
+ r#"{ "assistant": { "enable_experimental_live_diffs": true } }"#,
+ cx,
+ )
+ .unwrap()
+ });
cx.set_global(settings_store);
cx.update(language::init);
cx.update(Project::init_settings);
@@ -520,7 +528,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
»",
cx,
);
- expect_steps(
+ expect_patches(
&context,
"
@@ -539,17 +547,17 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
one
two
«
-
- Add a second function
-
- ```rust
- fn two() {}
- ```
-
+
»",
cx,
);
- expect_steps(
+ expect_patches(
&context,
"
one
two
- «
- Add a second function
-
- ```rust
- fn two() {}
- ```
-
+ «
»",
&[&[]],
cx,
);
- // The full suggestion is added
+ // The full patch is added
edit(
&context,
"
@@ -600,51 +596,46 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
one
two
-
- Add a second function
-
- ```rust
- fn two() {}
- ```
-
+
«
+ add a `two` function
src/lib.rs
insert_after
- fn one
- add a `two` function
+ fn one
+
+ fn two() {}
+
-
+
also,»",
cx,
);
- expect_steps(
+ expect_patches(
&context,
"
one
two
- «
- Add a second function
-
- ```rust
- fn two() {}
- ```
-
+ «
+ add a `two` function
src/lib.rs
insert_after
- fn one
- add a `two` function
+ fn one
+
+ fn two() {}
+
- »
-
+
+ »
also,",
- &[&[WorkflowStepEdit {
+ &[&[AssistantEdit {
path: "src/lib.rs".into(),
- kind: WorkflowStepEditKind::InsertAfter {
- search: "fn one".into(),
+ kind: AssistantEditKind::InsertAfter {
+ old_text: "fn one".into(),
+ new_text: "fn two() {}".into(),
description: "add a `two` function".into(),
},
}]],
@@ -659,51 +650,46 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
one
two
-
- Add a second function
-
- ```rust
- fn two() {}
- ```
-
+
+ add a `two` function
src/lib.rs
insert_after
- «fn zero»
- add a `two` function
+ «fn zero»
+
+ fn two() {}
+
-
+
also,",
cx,
);
- expect_steps(
+ expect_patches(
&context,
"
one
two
- «
- Add a second function
-
- ```rust
- fn two() {}
- ```
-
+ «
+ add a `two` function
src/lib.rs
insert_after
- fn zero
- add a `two` function
+ fn zero
+
+ fn two() {}
+
- »
-
+
+ »
also,",
- &[&[WorkflowStepEdit {
+ &[&[AssistantEdit {
path: "src/lib.rs".into(),
- kind: WorkflowStepEditKind::InsertAfter {
- search: "fn zero".into(),
+ kind: AssistantEditKind::InsertAfter {
+ old_text: "fn zero".into(),
+ new_text: "fn two() {}".into(),
description: "add a `two` function".into(),
},
}]],
@@ -715,27 +701,24 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
});
- expect_steps(
+ expect_patches(
&context,
"
one
two
-
- Add a second function
-
- ```rust
- fn two() {}
- ```
-
+
+ add a `two` function
src/lib.rs
insert_after
- fn zero
- add a `two` function
+ fn zero
+
+ fn two() {}
+
-
+
also,",
&[],
@@ -746,33 +729,31 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
context.update(cx, |context, cx| {
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
});
- expect_steps(
+ expect_patches(
&context,
"
one
two
- «
- Add a second function
-
- ```rust
- fn two() {}
- ```
-
+ «
+ add a `two` function
src/lib.rs
insert_after
- fn zero
- add a `two` function
+ fn zero
+
+ fn two() {}
+
- »
-
+
+ »
also,",
- &[&[WorkflowStepEdit {
+ &[&[AssistantEdit {
path: "src/lib.rs".into(),
- kind: WorkflowStepEditKind::InsertAfter {
- search: "fn zero".into(),
+ kind: AssistantEditKind::InsertAfter {
+ old_text: "fn zero".into(),
+ new_text: "fn two() {}".into(),
description: "add a `two` function".into(),
},
}]],
@@ -792,33 +773,31 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
cx,
)
});
- expect_steps(
+ expect_patches(
&deserialized_context,
"
one
two
- «
- Add a second function
-
- ```rust
- fn two() {}
- ```
-
+ «
+ add a `two` function
src/lib.rs
insert_after
- fn zero
- add a `two` function
+ fn zero
+
+ fn two() {}
+
- »
-
+
+ »
also,",
- &[&[WorkflowStepEdit {
+ &[&[AssistantEdit {
path: "src/lib.rs".into(),
- kind: WorkflowStepEditKind::InsertAfter {
- search: "fn zero".into(),
+ kind: AssistantEditKind::InsertAfter {
+ old_text: "fn zero".into(),
+ new_text: "fn two() {}".into(),
description: "add a `two` function".into(),
},
}]],
@@ -834,48 +813,58 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
cx.executor().run_until_parked();
}
- fn expect_steps(
+ #[track_caller]
+ fn expect_patches(
context: &Model,
expected_marked_text: &str,
- expected_suggestions: &[&[WorkflowStepEdit]],
+ expected_suggestions: &[&[AssistantEdit]],
cx: &mut TestAppContext,
) {
- context.update(cx, |context, cx| {
- let expected_marked_text = expected_marked_text.unindent();
- let (expected_text, expected_ranges) = marked_text_ranges(&expected_marked_text, false);
+ let expected_marked_text = expected_marked_text.unindent();
+ let (expected_text, _) = marked_text_ranges(&expected_marked_text, false);
+
+ let (buffer_text, ranges, patches) = context.update(cx, |context, cx| {
context.buffer.read_with(cx, |buffer, _| {
- assert_eq!(buffer.text(), expected_text);
let ranges = context
- .workflow_steps
+ .patches
.iter()
.map(|entry| entry.range.to_offset(buffer))
.collect::>();
- let marked = generate_marked_text(&expected_text, &ranges, false);
- assert_eq!(
- marked,
- expected_marked_text,
- "unexpected suggestion ranges. actual: {ranges:?}, expected: {expected_ranges:?}"
- );
- let suggestions = context
- .workflow_steps
- .iter()
- .map(|step| {
- step.edits
- .iter()
- .map(|edit| {
- let edit = edit.as_ref().unwrap();
- WorkflowStepEdit {
- path: edit.path.clone(),
- kind: edit.kind.clone(),
- }
- })
- .collect::>()
- })
- .collect::>();
-
- assert_eq!(suggestions, expected_suggestions);
- });
+ (
+ buffer.text(),
+ ranges,
+ context
+ .patches
+ .iter()
+ .map(|step| step.edits.clone())
+ .collect::>(),
+ )
+ })
});
+
+ assert_eq!(buffer_text, expected_text);
+
+ let actual_marked_text = generate_marked_text(&expected_text, &ranges, false);
+ assert_eq!(actual_marked_text, expected_marked_text);
+
+ assert_eq!(
+ patches
+ .iter()
+ .map(|patch| {
+ patch
+ .iter()
+ .map(|edit| {
+ let edit = edit.as_ref().unwrap();
+ AssistantEdit {
+ path: edit.path.clone(),
+ kind: edit.kind.clone(),
+ }
+ })
+ .collect::>()
+ })
+ .collect::>(),
+ expected_suggestions
+ );
}
}
diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs
index ce01e63b51..a11d4113d8 100644
--- a/crates/assistant/src/inline_assistant.rs
+++ b/crates/assistant/src/inline_assistant.rs
@@ -82,13 +82,6 @@ pub struct InlineAssistant {
assists: HashMap,
assists_by_editor: HashMap, EditorInlineAssists>,
assist_groups: HashMap,
- assist_observations: HashMap<
- InlineAssistId,
- (
- async_watch::Sender,
- async_watch::Receiver,
- ),
- >,
confirmed_assists: HashMap>,
prompt_history: VecDeque,
prompt_builder: Arc,
@@ -96,19 +89,6 @@ pub struct InlineAssistant {
fs: Arc,
}
-pub enum AssistStatus {
- Idle,
- Started,
- Stopped,
- Finished,
-}
-
-impl AssistStatus {
- pub fn is_done(&self) -> bool {
- matches!(self, Self::Stopped | Self::Finished)
- }
-}
-
impl Global for InlineAssistant {}
impl InlineAssistant {
@@ -123,7 +103,6 @@ impl InlineAssistant {
assists: HashMap::default(),
assists_by_editor: HashMap::default(),
assist_groups: HashMap::default(),
- assist_observations: HashMap::default(),
confirmed_assists: HashMap::default(),
prompt_history: VecDeque::default(),
prompt_builder,
@@ -835,17 +814,6 @@ impl InlineAssistant {
.insert(assist_id, confirmed_alternative);
}
}
-
- // Remove the assist from the status updates map
- self.assist_observations.remove(&assist_id);
- }
-
- pub fn undo_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool {
- let Some(codegen) = self.confirmed_assists.remove(&assist_id) else {
- return false;
- };
- codegen.update(cx, |this, cx| this.undo(cx));
- true
}
fn dismiss_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool {
@@ -1039,10 +1007,6 @@ impl InlineAssistant {
codegen.start(user_prompt, assistant_panel_context, cx)
})
.log_err();
-
- if let Some((tx, _)) = self.assist_observations.get(&assist_id) {
- tx.send(AssistStatus::Started).ok();
- }
}
pub fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
@@ -1053,25 +1017,6 @@ impl InlineAssistant {
};
assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
-
- if let Some((tx, _)) = self.assist_observations.get(&assist_id) {
- tx.send(AssistStatus::Stopped).ok();
- }
- }
-
- pub fn assist_status(&self, assist_id: InlineAssistId, cx: &AppContext) -> InlineAssistStatus {
- if let Some(assist) = self.assists.get(&assist_id) {
- match assist.codegen.read(cx).status(cx) {
- CodegenStatus::Idle => InlineAssistStatus::Idle,
- CodegenStatus::Pending => InlineAssistStatus::Pending,
- CodegenStatus::Done => InlineAssistStatus::Done,
- CodegenStatus::Error(_) => InlineAssistStatus::Error,
- }
- } else if self.confirmed_assists.contains_key(&assist_id) {
- InlineAssistStatus::Confirmed
- } else {
- InlineAssistStatus::Canceled
- }
}
fn update_editor_highlights(&self, editor: &View, cx: &mut WindowContext) {
@@ -1257,42 +1202,6 @@ impl InlineAssistant {
.collect();
})
}
-
- pub fn observe_assist(
- &mut self,
- assist_id: InlineAssistId,
- ) -> async_watch::Receiver {
- if let Some((_, rx)) = self.assist_observations.get(&assist_id) {
- rx.clone()
- } else {
- let (tx, rx) = async_watch::channel(AssistStatus::Idle);
- self.assist_observations.insert(assist_id, (tx, rx.clone()));
- rx
- }
- }
-}
-
-pub enum InlineAssistStatus {
- Idle,
- Pending,
- Done,
- Error,
- Confirmed,
- Canceled,
-}
-
-impl InlineAssistStatus {
- pub(crate) fn is_pending(&self) -> bool {
- matches!(self, Self::Pending)
- }
-
- pub(crate) fn is_confirmed(&self) -> bool {
- matches!(self, Self::Confirmed)
- }
-
- pub(crate) fn is_done(&self) -> bool {
- matches!(self, Self::Done)
- }
}
struct EditorInlineAssists {
@@ -2290,8 +2199,6 @@ impl InlineAssist {
if assist.decorations.is_none() {
this.finish_assist(assist_id, false, cx);
- } else if let Some(tx) = this.assist_observations.get(&assist_id) {
- tx.0.send(AssistStatus::Finished).ok();
}
}
})
diff --git a/crates/assistant/src/patch.rs b/crates/assistant/src/patch.rs
new file mode 100644
index 0000000000..82c81d3b86
--- /dev/null
+++ b/crates/assistant/src/patch.rs
@@ -0,0 +1,746 @@
+use anyhow::{anyhow, Context as _, Result};
+use collections::HashMap;
+use editor::ProposedChangesEditor;
+use futures::{future, TryFutureExt as _};
+use gpui::{AppContext, AsyncAppContext, Model, SharedString};
+use language::{AutoindentMode, Buffer, BufferSnapshot};
+use project::{Project, ProjectPath};
+use std::{cmp, ops::Range, path::Path, sync::Arc};
+use text::{AnchorRangeExt as _, Bias, OffsetRangeExt as _, Point};
+
+#[derive(Clone, Debug)]
+pub(crate) struct AssistantPatch {
+ pub range: Range,
+ pub title: SharedString,
+ pub edits: Arc<[Result]>,
+ pub status: AssistantPatchStatus,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub(crate) enum AssistantPatchStatus {
+ Pending,
+ Ready,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub(crate) struct AssistantEdit {
+ pub path: String,
+ pub kind: AssistantEditKind,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum AssistantEditKind {
+ Update {
+ old_text: String,
+ new_text: String,
+ description: String,
+ },
+ Create {
+ new_text: String,
+ description: String,
+ },
+ InsertBefore {
+ old_text: String,
+ new_text: String,
+ description: String,
+ },
+ InsertAfter {
+ old_text: String,
+ new_text: String,
+ description: String,
+ },
+ Delete {
+ old_text: String,
+ },
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub(crate) struct ResolvedPatch {
+ pub edit_groups: HashMap, Vec>,
+ pub errors: Vec,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct ResolvedEditGroup {
+ pub context_range: Range,
+ pub edits: Vec,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct ResolvedEdit {
+ range: Range,
+ new_text: String,
+ description: Option,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub(crate) struct AssistantPatchResolutionError {
+ pub edit_ix: usize,
+ pub message: String,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
+enum SearchDirection {
+ Up,
+ Left,
+ Diagonal,
+}
+
+// A measure of the currently quality of an in-progress fuzzy search.
+//
+// Uses 60 bits to store a numeric cost, and 4 bits to store the preceding
+// operation in the search.
+#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
+struct SearchState {
+ score: u32,
+ direction: SearchDirection,
+}
+
+impl SearchState {
+ fn new(score: u32, direction: SearchDirection) -> Self {
+ Self { score, direction }
+ }
+}
+
+impl ResolvedPatch {
+ pub fn apply(&self, editor: &ProposedChangesEditor, cx: &mut AppContext) {
+ for (buffer, groups) in &self.edit_groups {
+ let branch = editor.branch_buffer_for_base(buffer).unwrap();
+ Self::apply_edit_groups(groups, &branch, cx);
+ }
+ editor.recalculate_all_buffer_diffs();
+ }
+
+ fn apply_edit_groups(
+ groups: &Vec,
+ buffer: &Model,
+ cx: &mut AppContext,
+ ) {
+ let mut edits = Vec::new();
+ for group in groups {
+ for suggestion in &group.edits {
+ edits.push((suggestion.range.clone(), suggestion.new_text.clone()));
+ }
+ }
+ buffer.update(cx, |buffer, cx| {
+ buffer.edit(
+ edits,
+ Some(AutoindentMode::Block {
+ original_indent_columns: Vec::new(),
+ }),
+ cx,
+ );
+ });
+ }
+}
+
+impl ResolvedEdit {
+ pub fn try_merge(&mut self, other: &Self, buffer: &text::BufferSnapshot) -> bool {
+ let range = &self.range;
+ let other_range = &other.range;
+
+ // Don't merge if we don't contain the other suggestion.
+ if range.start.cmp(&other_range.start, buffer).is_gt()
+ || range.end.cmp(&other_range.end, buffer).is_lt()
+ {
+ return false;
+ }
+
+ if let Some(description) = &mut self.description {
+ if let Some(other_description) = &other.description {
+ description.push('\n');
+ description.push_str(other_description);
+ }
+ }
+ true
+ }
+}
+
+impl AssistantEdit {
+ pub fn new(
+ path: Option,
+ operation: Option,
+ old_text: Option,
+ new_text: Option,
+ description: Option,
+ ) -> Result {
+ let path = path.ok_or_else(|| anyhow!("missing path"))?;
+ let operation = operation.ok_or_else(|| anyhow!("missing operation"))?;
+
+ let kind = match operation.as_str() {
+ "update" => AssistantEditKind::Update {
+ old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
+ new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
+ description: description.ok_or_else(|| anyhow!("missing description"))?,
+ },
+ "insert_before" => AssistantEditKind::InsertBefore {
+ old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
+ new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
+ description: description.ok_or_else(|| anyhow!("missing description"))?,
+ },
+ "insert_after" => AssistantEditKind::InsertAfter {
+ old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
+ new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
+ description: description.ok_or_else(|| anyhow!("missing description"))?,
+ },
+ "delete" => AssistantEditKind::Delete {
+ old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
+ },
+ "create" => AssistantEditKind::Create {
+ description: description.ok_or_else(|| anyhow!("missing description"))?,
+ new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
+ },
+ _ => Err(anyhow!("unknown operation {operation:?}"))?,
+ };
+
+ Ok(Self { path, kind })
+ }
+
+ pub async fn resolve(
+ &self,
+ project: Model,
+ mut cx: AsyncAppContext,
+ ) -> Result<(Model, ResolvedEdit)> {
+ let path = self.path.clone();
+ let kind = self.kind.clone();
+ let buffer = project
+ .update(&mut cx, |project, cx| {
+ let project_path = project
+ .find_project_path(Path::new(&path), cx)
+ .or_else(|| {
+ // If we couldn't find a project path for it, put it in the active worktree
+ // so that when we create the buffer, it can be saved.
+ let worktree = project
+ .active_entry()
+ .and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
+ .or_else(|| project.worktrees(cx).next())?;
+ let worktree = worktree.read(cx);
+
+ Some(ProjectPath {
+ worktree_id: worktree.id(),
+ path: Arc::from(Path::new(&path)),
+ })
+ })
+ .with_context(|| format!("worktree not found for {:?}", path))?;
+ anyhow::Ok(project.open_buffer(project_path, cx))
+ })??
+ .await?;
+
+ let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
+ let suggestion = cx
+ .background_executor()
+ .spawn(async move { kind.resolve(&snapshot) })
+ .await;
+
+ Ok((buffer, suggestion))
+ }
+}
+
+impl AssistantEditKind {
+ fn resolve(self, snapshot: &BufferSnapshot) -> ResolvedEdit {
+ match self {
+ Self::Update {
+ old_text,
+ new_text,
+ description,
+ } => {
+ let range = Self::resolve_location(&snapshot, &old_text);
+ ResolvedEdit {
+ range,
+ new_text,
+ description: Some(description),
+ }
+ }
+ Self::Create {
+ new_text,
+ description,
+ } => ResolvedEdit {
+ range: text::Anchor::MIN..text::Anchor::MAX,
+ description: Some(description),
+ new_text,
+ },
+ Self::InsertBefore {
+ old_text,
+ mut new_text,
+ description,
+ } => {
+ let range = Self::resolve_location(&snapshot, &old_text);
+ new_text.push('\n');
+ ResolvedEdit {
+ range: range.start..range.start,
+ new_text,
+ description: Some(description),
+ }
+ }
+ Self::InsertAfter {
+ old_text,
+ mut new_text,
+ description,
+ } => {
+ let range = Self::resolve_location(&snapshot, &old_text);
+ new_text.insert(0, '\n');
+ ResolvedEdit {
+ range: range.end..range.end,
+ new_text,
+ description: Some(description),
+ }
+ }
+ Self::Delete { old_text } => {
+ let range = Self::resolve_location(&snapshot, &old_text);
+ ResolvedEdit {
+ range,
+ new_text: String::new(),
+ description: None,
+ }
+ }
+ }
+ }
+
+ fn resolve_location(buffer: &text::BufferSnapshot, search_query: &str) -> Range {
+ const INSERTION_COST: u32 = 3;
+ const WHITESPACE_INSERTION_COST: u32 = 1;
+ const DELETION_COST: u32 = 3;
+ const WHITESPACE_DELETION_COST: u32 = 1;
+ const EQUALITY_BONUS: u32 = 5;
+
+ struct Matrix {
+ cols: usize,
+ data: Vec,
+ }
+
+ impl Matrix {
+ fn new(rows: usize, cols: usize) -> Self {
+ Matrix {
+ cols,
+ data: vec![SearchState::new(0, SearchDirection::Diagonal); rows * cols],
+ }
+ }
+
+ fn get(&self, row: usize, col: usize) -> SearchState {
+ self.data[row * self.cols + col]
+ }
+
+ fn set(&mut self, row: usize, col: usize, cost: SearchState) {
+ self.data[row * self.cols + col] = cost;
+ }
+ }
+
+ let buffer_len = buffer.len();
+ let query_len = search_query.len();
+ let mut matrix = Matrix::new(query_len + 1, buffer_len + 1);
+
+ for (row, query_byte) in search_query.bytes().enumerate() {
+ for (col, buffer_byte) in buffer.bytes_in_range(0..buffer.len()).flatten().enumerate() {
+ let deletion_cost = if query_byte.is_ascii_whitespace() {
+ WHITESPACE_DELETION_COST
+ } else {
+ DELETION_COST
+ };
+ let insertion_cost = if buffer_byte.is_ascii_whitespace() {
+ WHITESPACE_INSERTION_COST
+ } else {
+ INSERTION_COST
+ };
+
+ let up = SearchState::new(
+ matrix.get(row, col + 1).score.saturating_sub(deletion_cost),
+ SearchDirection::Up,
+ );
+ let left = SearchState::new(
+ matrix
+ .get(row + 1, col)
+ .score
+ .saturating_sub(insertion_cost),
+ SearchDirection::Left,
+ );
+ let diagonal = SearchState::new(
+ if query_byte == *buffer_byte {
+ matrix.get(row, col).score.saturating_add(EQUALITY_BONUS)
+ } else {
+ matrix
+ .get(row, col)
+ .score
+ .saturating_sub(deletion_cost + insertion_cost)
+ },
+ SearchDirection::Diagonal,
+ );
+ matrix.set(row + 1, col + 1, up.max(left).max(diagonal));
+ }
+ }
+
+ // Traceback to find the best match
+ let mut best_buffer_end = buffer_len;
+ let mut best_score = 0;
+ for col in 1..=buffer_len {
+ let score = matrix.get(query_len, col).score;
+ if score > best_score {
+ best_score = score;
+ best_buffer_end = col;
+ }
+ }
+
+ let mut query_ix = query_len;
+ let mut buffer_ix = best_buffer_end;
+ while query_ix > 0 && buffer_ix > 0 {
+ let current = matrix.get(query_ix, buffer_ix);
+ match current.direction {
+ SearchDirection::Diagonal => {
+ query_ix -= 1;
+ buffer_ix -= 1;
+ }
+ SearchDirection::Up => {
+ query_ix -= 1;
+ }
+ SearchDirection::Left => {
+ buffer_ix -= 1;
+ }
+ }
+ }
+
+ let mut start = buffer.offset_to_point(buffer.clip_offset(buffer_ix, Bias::Left));
+ start.column = 0;
+ let mut end = buffer.offset_to_point(buffer.clip_offset(best_buffer_end, Bias::Right));
+ if end.column > 0 {
+ end.column = buffer.line_len(end.row);
+ }
+
+ buffer.anchor_after(start)..buffer.anchor_before(end)
+ }
+}
+
+impl AssistantPatch {
+ pub(crate) async fn resolve(
+ &self,
+ project: Model,
+ cx: &mut AsyncAppContext,
+ ) -> ResolvedPatch {
+ let mut resolve_tasks = Vec::new();
+ for (ix, edit) in self.edits.iter().enumerate() {
+ if let Ok(edit) = edit.as_ref() {
+ resolve_tasks.push(
+ edit.resolve(project.clone(), cx.clone())
+ .map_err(move |error| (ix, error)),
+ );
+ }
+ }
+
+ let edits = future::join_all(resolve_tasks).await;
+ let mut errors = Vec::new();
+ let mut edits_by_buffer = HashMap::default();
+ for entry in edits {
+ match entry {
+ Ok((buffer, edit)) => {
+ edits_by_buffer
+ .entry(buffer)
+ .or_insert_with(Vec::new)
+ .push(edit);
+ }
+ Err((edit_ix, error)) => errors.push(AssistantPatchResolutionError {
+ edit_ix,
+ message: error.to_string(),
+ }),
+ }
+ }
+
+ // Expand the context ranges of each edit and group edits with overlapping context ranges.
+ let mut edit_groups_by_buffer = HashMap::default();
+ for (buffer, edits) in edits_by_buffer {
+ if let Ok(snapshot) = buffer.update(cx, |buffer, _| buffer.text_snapshot()) {
+ edit_groups_by_buffer.insert(buffer, Self::group_edits(edits, &snapshot));
+ }
+ }
+
+ ResolvedPatch {
+ edit_groups: edit_groups_by_buffer,
+ errors,
+ }
+ }
+
+ fn group_edits(
+ mut edits: Vec,
+ snapshot: &text::BufferSnapshot,
+ ) -> Vec {
+ let mut edit_groups = Vec::::new();
+ // Sort edits by their range so that earlier, larger ranges come first
+ edits.sort_by(|a, b| a.range.cmp(&b.range, &snapshot));
+
+ // Merge overlapping edits
+ edits.dedup_by(|a, b| b.try_merge(a, &snapshot));
+
+ // Create context ranges for each edit
+ for edit in edits {
+ let context_range = {
+ let edit_point_range = edit.range.to_point(&snapshot);
+ let start_row = edit_point_range.start.row.saturating_sub(5);
+ let end_row = cmp::min(edit_point_range.end.row + 5, snapshot.max_point().row);
+ let start = snapshot.anchor_before(Point::new(start_row, 0));
+ let end = snapshot.anchor_after(Point::new(end_row, snapshot.line_len(end_row)));
+ start..end
+ };
+
+ if let Some(last_group) = edit_groups.last_mut() {
+ if last_group
+ .context_range
+ .end
+ .cmp(&context_range.start, &snapshot)
+ .is_ge()
+ {
+ // Merge with the previous group if context ranges overlap
+ last_group.context_range.end = context_range.end;
+ last_group.edits.push(edit);
+ } else {
+ // Create a new group
+ edit_groups.push(ResolvedEditGroup {
+ context_range,
+ edits: vec![edit],
+ });
+ }
+ } else {
+ // Create the first group
+ edit_groups.push(ResolvedEditGroup {
+ context_range,
+ edits: vec![edit],
+ });
+ }
+ }
+
+ edit_groups
+ }
+
+ pub fn path_count(&self) -> usize {
+ self.paths().count()
+ }
+
+ pub fn paths(&self) -> impl '_ + Iterator
- {
+ let mut prev_path = None;
+ self.edits.iter().filter_map(move |edit| {
+ if let Ok(edit) = edit {
+ let path = Some(edit.path.as_str());
+ if path != prev_path {
+ prev_path = path;
+ return path;
+ }
+ }
+ None
+ })
+ }
+}
+
+impl PartialEq for AssistantPatch {
+ fn eq(&self, other: &Self) -> bool {
+ self.range == other.range
+ && self.title == other.title
+ && Arc::ptr_eq(&self.edits, &other.edits)
+ }
+}
+
+impl Eq for AssistantPatch {}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use gpui::{AppContext, Context};
+ use language::{
+ language_settings::AllLanguageSettings, Language, LanguageConfig, LanguageMatcher,
+ };
+ use settings::SettingsStore;
+ use text::{OffsetRangeExt, Point};
+ use ui::BorrowAppContext;
+ use unindent::Unindent as _;
+
+ #[gpui::test]
+ fn test_resolve_location(cx: &mut AppContext) {
+ {
+ let buffer = cx.new_model(|cx| {
+ Buffer::local(
+ concat!(
+ " Lorem\n",
+ " ipsum\n",
+ " dolor sit amet\n",
+ " consecteur",
+ ),
+ cx,
+ )
+ });
+ let snapshot = buffer.read(cx).snapshot();
+ assert_eq!(
+ AssistantEditKind::resolve_location(&snapshot, "ipsum\ndolor").to_point(&snapshot),
+ Point::new(1, 0)..Point::new(2, 18)
+ );
+ }
+
+ {
+ let buffer = cx.new_model(|cx| {
+ Buffer::local(
+ concat!(
+ "fn foo1(a: usize) -> usize {\n",
+ " 40\n",
+ "}\n",
+ "\n",
+ "fn foo2(b: usize) -> usize {\n",
+ " 42\n",
+ "}\n",
+ ),
+ cx,
+ )
+ });
+ let snapshot = buffer.read(cx).snapshot();
+ assert_eq!(
+ AssistantEditKind::resolve_location(&snapshot, "fn foo1(b: usize) {\n40\n}")
+ .to_point(&snapshot),
+ Point::new(0, 0)..Point::new(2, 1)
+ );
+ }
+
+ {
+ let buffer = cx.new_model(|cx| {
+ Buffer::local(
+ concat!(
+ "fn main() {\n",
+ " Foo\n",
+ " .bar()\n",
+ " .baz()\n",
+ " .qux()\n",
+ "}\n",
+ "\n",
+ "fn foo2(b: usize) -> usize {\n",
+ " 42\n",
+ "}\n",
+ ),
+ cx,
+ )
+ });
+ let snapshot = buffer.read(cx).snapshot();
+ assert_eq!(
+ AssistantEditKind::resolve_location(&snapshot, "Foo.bar.baz.qux()")
+ .to_point(&snapshot),
+ Point::new(1, 0)..Point::new(4, 14)
+ );
+ }
+ }
+
+ #[gpui::test]
+ fn test_resolve_edits(cx: &mut AppContext) {
+ let settings_store = SettingsStore::test(cx);
+ cx.set_global(settings_store);
+ language::init(cx);
+ cx.update_global::(|settings, cx| {
+ settings.update_user_settings::(cx, |_| {});
+ });
+
+ assert_edits(
+ "
+ /// A person
+ struct Person {
+ name: String,
+ age: usize,
+ }
+
+ /// A dog
+ struct Dog {
+ weight: f32,
+ }
+
+ impl Person {
+ fn name(&self) -> &str {
+ &self.name
+ }
+ }
+ "
+ .unindent(),
+ vec![
+ AssistantEditKind::Update {
+ old_text: "
+ name: String,
+ "
+ .unindent(),
+ new_text: "
+ first_name: String,
+ last_name: String,
+ "
+ .unindent(),
+ description: "".into(),
+ },
+ AssistantEditKind::Update {
+ old_text: "
+ fn name(&self) -> &str {
+ &self.name
+ }
+ "
+ .unindent(),
+ new_text: "
+ fn name(&self) -> String {
+ format!(\"{} {}\", self.first_name, self.last_name)
+ }
+ "
+ .unindent(),
+ description: "".into(),
+ },
+ ],
+ "
+ /// A person
+ struct Person {
+ first_name: String,
+ last_name: String,
+ age: usize,
+ }
+
+ /// A dog
+ struct Dog {
+ weight: f32,
+ }
+
+ impl Person {
+ fn name(&self) -> String {
+ format!(\"{} {}\", self.first_name, self.last_name)
+ }
+ }
+ "
+ .unindent(),
+ cx,
+ );
+ }
+
+ #[track_caller]
+ fn assert_edits(
+ old_text: String,
+ edits: Vec,
+ new_text: String,
+ cx: &mut AppContext,
+ ) {
+ let buffer =
+ cx.new_model(|cx| Buffer::local(old_text, cx).with_language(Arc::new(rust_lang()), cx));
+ let snapshot = buffer.read(cx).snapshot();
+ let resolved_edits = edits
+ .into_iter()
+ .map(|kind| kind.resolve(&snapshot))
+ .collect();
+ let edit_groups = AssistantPatch::group_edits(resolved_edits, &snapshot);
+ ResolvedPatch::apply_edit_groups(&edit_groups, &buffer, cx);
+ let actual_new_text = buffer.read(cx).text();
+ pretty_assertions::assert_eq!(actual_new_text, new_text);
+ }
+
+ fn rust_lang() -> Language {
+ Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ matcher: LanguageMatcher {
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ Some(language::tree_sitter_rust::LANGUAGE.into()),
+ )
+ .with_indents_query(
+ r#"
+ (call_expression) @indent
+ (field_expression) @indent
+ (_ "(" ")" @end) @indent
+ (_ "{" "}" @end) @indent
+ "#,
+ )
+ .unwrap()
+ }
+}
diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs
index 106935cb88..132b3df68f 100644
--- a/crates/assistant/src/prompts.rs
+++ b/crates/assistant/src/prompts.rs
@@ -45,15 +45,6 @@ pub struct ProjectSlashCommandPromptContext {
pub context_buffer: String,
}
-/// Context required to generate a workflow step resolution prompt.
-#[derive(Debug, Serialize)]
-pub struct StepResolutionContext {
- /// The full context, including ... tags
- pub workflow_context: String,
- /// The text of the specific step from the context to resolve
- pub step_to_resolve: String,
-}
-
pub struct PromptLoadingParams<'a> {
pub fs: Arc,
pub repo_path: Option,
diff --git a/crates/assistant/src/slash_command/workflow_command.rs b/crates/assistant/src/slash_command/workflow_command.rs
index 071b4feaf4..50c0e6cbc6 100644
--- a/crates/assistant/src/slash_command/workflow_command.rs
+++ b/crates/assistant/src/slash_command/workflow_command.rs
@@ -18,6 +18,8 @@ pub(crate) struct WorkflowSlashCommand {
}
impl WorkflowSlashCommand {
+ pub const NAME: &'static str = "workflow";
+
pub fn new(prompt_builder: Arc) -> Self {
Self { prompt_builder }
}
@@ -25,7 +27,7 @@ impl WorkflowSlashCommand {
impl SlashCommand for WorkflowSlashCommand {
fn name(&self) -> String {
- "workflow".into()
+ Self::NAME.into()
}
fn description(&self) -> String {
diff --git a/crates/assistant/src/workflow.rs b/crates/assistant/src/workflow.rs
deleted file mode 100644
index 8a770e21aa..0000000000
--- a/crates/assistant/src/workflow.rs
+++ /dev/null
@@ -1,507 +0,0 @@
-use crate::{AssistantPanel, InlineAssistId, InlineAssistant};
-use anyhow::{anyhow, Context as _, Result};
-use collections::HashMap;
-use editor::Editor;
-use gpui::AsyncAppContext;
-use gpui::{Model, Task, UpdateGlobal as _, View, WeakView, WindowContext};
-use language::{Buffer, BufferSnapshot};
-use project::{Project, ProjectPath};
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use std::{ops::Range, path::Path, sync::Arc};
-use text::Bias;
-use workspace::Workspace;
-
-#[derive(Debug)]
-pub(crate) struct WorkflowStep {
- pub range: Range,
- pub leading_tags_end: text::Anchor,
- pub trailing_tag_start: Option,
- pub edits: Arc<[Result]>,
- pub resolution_task: Option>,
- pub resolution: Option>>,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub(crate) struct WorkflowStepEdit {
- pub path: String,
- pub kind: WorkflowStepEditKind,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub(crate) struct WorkflowStepResolution {
- pub title: String,
- pub suggestion_groups: HashMap, Vec>,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct WorkflowSuggestionGroup {
- pub context_range: Range,
- pub suggestions: Vec,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub enum WorkflowSuggestion {
- Update {
- range: Range,
- description: String,
- },
- CreateFile {
- description: String,
- },
- InsertBefore {
- position: language::Anchor,
- description: String,
- },
- InsertAfter {
- position: language::Anchor,
- description: String,
- },
- Delete {
- range: Range,
- },
-}
-
-impl WorkflowSuggestion {
- pub fn range(&self) -> Range {
- match self {
- Self::Update { range, .. } => range.clone(),
- Self::CreateFile { .. } => language::Anchor::MIN..language::Anchor::MAX,
- Self::InsertBefore { position, .. } | Self::InsertAfter { position, .. } => {
- *position..*position
- }
- Self::Delete { range, .. } => range.clone(),
- }
- }
-
- pub fn description(&self) -> Option<&str> {
- match self {
- Self::Update { description, .. }
- | Self::CreateFile { description }
- | Self::InsertBefore { description, .. }
- | Self::InsertAfter { description, .. } => Some(description),
- Self::Delete { .. } => None,
- }
- }
-
- fn description_mut(&mut self) -> Option<&mut String> {
- match self {
- Self::Update { description, .. }
- | Self::CreateFile { description }
- | Self::InsertBefore { description, .. }
- | Self::InsertAfter { description, .. } => Some(description),
- Self::Delete { .. } => None,
- }
- }
-
- pub fn try_merge(&mut self, other: &Self, buffer: &BufferSnapshot) -> bool {
- let range = self.range();
- let other_range = other.range();
-
- // Don't merge if we don't contain the other suggestion.
- if range.start.cmp(&other_range.start, buffer).is_gt()
- || range.end.cmp(&other_range.end, buffer).is_lt()
- {
- return false;
- }
-
- if let Some(description) = self.description_mut() {
- if let Some(other_description) = other.description() {
- description.push('\n');
- description.push_str(other_description);
- }
- }
- true
- }
-
- pub fn show(
- &self,
- editor: &View,
- excerpt_id: editor::ExcerptId,
- workspace: &WeakView,
- assistant_panel: &View,
- cx: &mut WindowContext,
- ) -> Option {
- let mut initial_transaction_id = None;
- let initial_prompt;
- let suggestion_range;
- let buffer = editor.read(cx).buffer().clone();
- let snapshot = buffer.read(cx).snapshot(cx);
-
- match self {
- Self::Update {
- range, description, ..
- } => {
- initial_prompt = description.clone();
- suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
- ..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
- }
- Self::CreateFile { description } => {
- initial_prompt = description.clone();
- suggestion_range = editor::Anchor::min()..editor::Anchor::min();
- }
- Self::InsertBefore {
- position,
- description,
- ..
- } => {
- let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
- initial_prompt = description.clone();
- suggestion_range = buffer.update(cx, |buffer, cx| {
- buffer.start_transaction(cx);
- let line_start = buffer.insert_empty_line(position, true, true, cx);
- initial_transaction_id = buffer.end_transaction(cx);
- buffer.refresh_preview(cx);
-
- let line_start = buffer.read(cx).anchor_before(line_start);
- line_start..line_start
- });
- }
- Self::InsertAfter {
- position,
- description,
- ..
- } => {
- let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
- initial_prompt = description.clone();
- suggestion_range = buffer.update(cx, |buffer, cx| {
- buffer.start_transaction(cx);
- let line_start = buffer.insert_empty_line(position, true, true, cx);
- initial_transaction_id = buffer.end_transaction(cx);
- buffer.refresh_preview(cx);
-
- let line_start = buffer.read(cx).anchor_before(line_start);
- line_start..line_start
- });
- }
- Self::Delete { range, .. } => {
- initial_prompt = "Delete".to_string();
- suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
- ..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
- }
- }
-
- InlineAssistant::update_global(cx, |inline_assistant, cx| {
- Some(inline_assistant.suggest_assist(
- editor,
- suggestion_range,
- initial_prompt,
- initial_transaction_id,
- false,
- Some(workspace.clone()),
- Some(assistant_panel),
- cx,
- ))
- })
- }
-}
-
-impl WorkflowStepEdit {
- pub fn new(
- path: Option,
- operation: Option,
- search: Option,
- description: Option,
- ) -> Result {
- let path = path.ok_or_else(|| anyhow!("missing path"))?;
- let operation = operation.ok_or_else(|| anyhow!("missing operation"))?;
-
- let kind = match operation.as_str() {
- "update" => WorkflowStepEditKind::Update {
- search: search.ok_or_else(|| anyhow!("missing search"))?,
- description: description.ok_or_else(|| anyhow!("missing description"))?,
- },
- "insert_before" => WorkflowStepEditKind::InsertBefore {
- search: search.ok_or_else(|| anyhow!("missing search"))?,
- description: description.ok_or_else(|| anyhow!("missing description"))?,
- },
- "insert_after" => WorkflowStepEditKind::InsertAfter {
- search: search.ok_or_else(|| anyhow!("missing search"))?,
- description: description.ok_or_else(|| anyhow!("missing description"))?,
- },
- "delete" => WorkflowStepEditKind::Delete {
- search: search.ok_or_else(|| anyhow!("missing search"))?,
- },
- "create" => WorkflowStepEditKind::Create {
- description: description.ok_or_else(|| anyhow!("missing description"))?,
- },
- _ => Err(anyhow!("unknown operation {operation:?}"))?,
- };
-
- Ok(Self { path, kind })
- }
-
- pub async fn resolve(
- &self,
- project: Model,
- mut cx: AsyncAppContext,
- ) -> Result<(Model, super::WorkflowSuggestion)> {
- let path = self.path.clone();
- let kind = self.kind.clone();
- let buffer = project
- .update(&mut cx, |project, cx| {
- let project_path = project
- .find_project_path(Path::new(&path), cx)
- .or_else(|| {
- // If we couldn't find a project path for it, put it in the active worktree
- // so that when we create the buffer, it can be saved.
- let worktree = project
- .active_entry()
- .and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
- .or_else(|| project.worktrees(cx).next())?;
- let worktree = worktree.read(cx);
-
- Some(ProjectPath {
- worktree_id: worktree.id(),
- path: Arc::from(Path::new(&path)),
- })
- })
- .with_context(|| format!("worktree not found for {:?}", path))?;
- anyhow::Ok(project.open_buffer(project_path, cx))
- })??
- .await?;
-
- let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
- let suggestion = cx
- .background_executor()
- .spawn(async move {
- match kind {
- WorkflowStepEditKind::Update {
- search,
- description,
- } => {
- let range = Self::resolve_location(&snapshot, &search);
- WorkflowSuggestion::Update { range, description }
- }
- WorkflowStepEditKind::Create { description } => {
- WorkflowSuggestion::CreateFile { description }
- }
- WorkflowStepEditKind::InsertBefore {
- search,
- description,
- } => {
- let range = Self::resolve_location(&snapshot, &search);
- WorkflowSuggestion::InsertBefore {
- position: range.start,
- description,
- }
- }
- WorkflowStepEditKind::InsertAfter {
- search,
- description,
- } => {
- let range = Self::resolve_location(&snapshot, &search);
- WorkflowSuggestion::InsertAfter {
- position: range.end,
- description,
- }
- }
- WorkflowStepEditKind::Delete { search } => {
- let range = Self::resolve_location(&snapshot, &search);
- WorkflowSuggestion::Delete { range }
- }
- }
- })
- .await;
-
- Ok((buffer, suggestion))
- }
-
- fn resolve_location(buffer: &text::BufferSnapshot, search_query: &str) -> Range {
- const INSERTION_SCORE: f64 = -1.0;
- const DELETION_SCORE: f64 = -1.0;
- const REPLACEMENT_SCORE: f64 = -1.0;
- const EQUALITY_SCORE: f64 = 5.0;
-
- struct Matrix {
- cols: usize,
- data: Vec,
- }
-
- impl Matrix {
- fn new(rows: usize, cols: usize) -> Self {
- Matrix {
- cols,
- data: vec![0.0; rows * cols],
- }
- }
-
- fn get(&self, row: usize, col: usize) -> f64 {
- self.data[row * self.cols + col]
- }
-
- fn set(&mut self, row: usize, col: usize, value: f64) {
- self.data[row * self.cols + col] = value;
- }
- }
-
- let buffer_len = buffer.len();
- let query_len = search_query.len();
- let mut matrix = Matrix::new(query_len + 1, buffer_len + 1);
-
- for (i, query_byte) in search_query.bytes().enumerate() {
- for (j, buffer_byte) in buffer.bytes_in_range(0..buffer.len()).flatten().enumerate() {
- let match_score = if query_byte == *buffer_byte {
- EQUALITY_SCORE
- } else {
- REPLACEMENT_SCORE
- };
- let up = matrix.get(i + 1, j) + DELETION_SCORE;
- let left = matrix.get(i, j + 1) + INSERTION_SCORE;
- let diagonal = matrix.get(i, j) + match_score;
- let score = up.max(left.max(diagonal)).max(0.);
- matrix.set(i + 1, j + 1, score);
- }
- }
-
- // Traceback to find the best match
- let mut best_buffer_end = buffer_len;
- let mut best_score = 0.0;
- for col in 1..=buffer_len {
- let score = matrix.get(query_len, col);
- if score > best_score {
- best_score = score;
- best_buffer_end = col;
- }
- }
-
- let mut query_ix = query_len;
- let mut buffer_ix = best_buffer_end;
- while query_ix > 0 && buffer_ix > 0 {
- let current = matrix.get(query_ix, buffer_ix);
- let up = matrix.get(query_ix - 1, buffer_ix);
- let left = matrix.get(query_ix, buffer_ix - 1);
- if current == left + INSERTION_SCORE {
- buffer_ix -= 1;
- } else if current == up + DELETION_SCORE {
- query_ix -= 1;
- } else {
- query_ix -= 1;
- buffer_ix -= 1;
- }
- }
-
- let mut start = buffer.offset_to_point(buffer.clip_offset(buffer_ix, Bias::Left));
- start.column = 0;
- let mut end = buffer.offset_to_point(buffer.clip_offset(best_buffer_end, Bias::Right));
- end.column = buffer.line_len(end.row);
-
- buffer.anchor_after(start)..buffer.anchor_before(end)
- }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
-#[serde(tag = "operation")]
-pub enum WorkflowStepEditKind {
- /// Rewrites the specified text entirely based on the given description.
- /// This operation completely replaces the given text.
- Update {
- /// A string in the source text to apply the update to.
- search: String,
- /// A brief description of the transformation to apply to the symbol.
- description: String,
- },
- /// Creates a new file with the given path based on the provided description.
- /// This operation adds a new file to the codebase.
- Create {
- /// A brief description of the file to be created.
- description: String,
- },
- /// Inserts text before the specified text in the source file.
- InsertBefore {
- /// A string in the source text to insert text before.
- search: String,
- /// A brief description of how the new text should be generated.
- description: String,
- },
- /// Inserts text after the specified text in the source file.
- InsertAfter {
- /// A string in the source text to insert text after.
- search: String,
- /// A brief description of how the new text should be generated.
- description: String,
- },
- /// Deletes the specified symbol from the containing file.
- Delete {
- /// A string in the source text to delete.
- search: String,
- },
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use gpui::{AppContext, Context};
- use text::{OffsetRangeExt, Point};
-
- #[gpui::test]
- fn test_resolve_location(cx: &mut AppContext) {
- {
- let buffer = cx.new_model(|cx| {
- Buffer::local(
- concat!(
- " Lorem\n",
- " ipsum\n",
- " dolor sit amet\n",
- " consecteur",
- ),
- cx,
- )
- });
- let snapshot = buffer.read(cx).snapshot();
- assert_eq!(
- WorkflowStepEdit::resolve_location(&snapshot, "ipsum\ndolor").to_point(&snapshot),
- Point::new(1, 0)..Point::new(2, 18)
- );
- }
-
- {
- let buffer = cx.new_model(|cx| {
- Buffer::local(
- concat!(
- "fn foo1(a: usize) -> usize {\n",
- " 42\n",
- "}\n",
- "\n",
- "fn foo2(b: usize) -> usize {\n",
- " 42\n",
- "}\n",
- ),
- cx,
- )
- });
- let snapshot = buffer.read(cx).snapshot();
- assert_eq!(
- WorkflowStepEdit::resolve_location(&snapshot, "fn foo1(b: usize) {\n42\n}")
- .to_point(&snapshot),
- Point::new(0, 0)..Point::new(2, 1)
- );
- }
-
- {
- let buffer = cx.new_model(|cx| {
- Buffer::local(
- concat!(
- "fn main() {\n",
- " Foo\n",
- " .bar()\n",
- " .baz()\n",
- " .qux()\n",
- "}\n",
- "\n",
- "fn foo2(b: usize) -> usize {\n",
- " 42\n",
- "}\n",
- ),
- cx,
- )
- });
- let snapshot = buffer.read(cx).snapshot();
- assert_eq!(
- WorkflowStepEdit::resolve_location(&snapshot, "Foo.bar.baz.qux()")
- .to_point(&snapshot),
- Point::new(1, 0)..Point::new(4, 14)
- );
- }
- }
-}
diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs
index 72205273f8..9d9cfde7b9 100644
--- a/crates/editor/src/editor.rs
+++ b/crates/editor/src/editor.rs
@@ -100,7 +100,7 @@ use language::{
};
use linked_editing_ranges::refresh_linked_ranges;
pub use proposed_changes_editor::{
- ProposedChangesBuffer, ProposedChangesEditor, ProposedChangesEditorToolbar,
+ ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
};
use similar::{ChangeTag, TextDiff};
use task::{ResolvedTask, TaskTemplate, TaskVariables};
@@ -12363,10 +12363,15 @@ impl Editor {
let proposed_changes_buffers = new_selections_by_buffer
.into_iter()
- .map(|(buffer, ranges)| ProposedChangesBuffer { buffer, ranges })
+ .map(|(buffer, ranges)| ProposedChangeLocation { buffer, ranges })
.collect::>();
let proposed_changes_editor = cx.new_view(|cx| {
- ProposedChangesEditor::new(proposed_changes_buffers, self.project.clone(), cx)
+ ProposedChangesEditor::new(
+ "Proposed changes",
+ proposed_changes_buffers,
+ self.project.clone(),
+ cx,
+ )
});
cx.window_context().defer(move |cx| {
diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs
index 056daf28a8..2c99b9d277 100644
--- a/crates/editor/src/proposed_changes_editor.rs
+++ b/crates/editor/src/proposed_changes_editor.rs
@@ -16,16 +16,24 @@ use workspace::{
pub struct ProposedChangesEditor {
editor: View,
- _subscriptions: Vec,
+ multibuffer: Model,
+ title: SharedString,
+ buffer_entries: Vec,
_recalculate_diffs_task: Task