diff --git a/assets/prompts/edit_workflow.hbs b/assets/prompts/edit_workflow.hbs index a9cb07a650..c558bc20d0 100644 --- a/assets/prompts/edit_workflow.hbs +++ b/assets/prompts/edit_workflow.hbs @@ -27,17 +27,17 @@ impl Person { ``` - src/person.rs - insert_before - struct Person height - Add the age field +src/person.rs +insert_before +height: f32, +Add the age field - src/person.rs - append_child - impl Person - Add the age getter +src/person.rs +insert_after +impl Person { +Add the age getter @@ -45,15 +45,15 @@ impl Person { 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. -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: +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) +### `` (optional) -This tag contains the fully-qualified name of a symbol in the source file, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`. If not provided, the new content will be inserted at the top of the file. +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) @@ -62,110 +62,179 @@ This tag contains a single-line description of the edit that should be made at t ### `` (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 symbol entirely based on the given description. +- `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_sibling_before`: Inserts a new symbol based on the given description as a sibling before the specified symbol. -- `insert_sibling_after`: Inserts a new symbol based on the given description as a sibling after the specified symbol. -- `prepend_child`: Inserts a new symbol as a child of the specified symbol at the start. -- `append_child`: Inserts a new symbol as a child of the specified symbol at the end. -- `delete`: Deletes the specified symbol from the containing file. +- `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. - 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 edits on a parent symbol and one of its children in the same edit block. +- 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 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. +- 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. -- Always ensure imports are added if you're referencing symbols that are not in scope. To manipulate imports, produce an edit where the `"symbol"` key is set to `"#imports"` +- Always ensure imports are added if you're referencing symbols that are not in scope. Here are some concrete examples. - - + -```rs src/rectangle.rs -struct Rectangle { + +```rs src/shapes.rs +pub mod rectangle; +pub mod circle; +``` + +```rs src/shapes/rectangle.rs +pub struct Rectangle { width: f64, height: f64, } impl Rectangle { - fn new(width: f64, height: f64) -> Self { + pub 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? +```rs src/shapes/circle.rs +pub struct Circle { + radius: f64, +} + +impl Circle { + pub fn new(radius: f64) -> Self { + Circle { radius } + } +} +``` + +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 methods to calculate the area and perimeter of the rectangle +Add origin fields to both shape types. ```rust -impl Rectangle { - // existing methods... +struct Rectangle { + // existing fields ... + origin: (f64, f64), +} +``` - fn calculate_area(&self) -> f64 { - self.width * self.height - } - - fn calculate_perimeter(&self) -> f64 { - 2.0 * (self.width + self.height) - } +```rust +struct Circle { + // existing fields ... + origin: (f64, f64), } ``` - src/rectangle.rs - append_child - impl Rectangle - Add calculate_area and calculate_perimeter methods +src/shapes/rectangle.rs +insert_before + + width: f64, + height: f64, + +Add the origin field to Rectangle + + + +src/shapes/circle.rs +insert_before + + 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 + + + +src/shapes/circle.rs +update + + fn new(radius: f64) -> Self { + Circle { radius } + } + +Update the Circle new function to take an origin -Implement the Display trait for the Rectangle struct - -```rust -use std::fmt; - -impl fmt::Display for Rectangle { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Rectangle: {}x{}", self.width, self.height) - } -} -``` +Implement Display for both shapes - src/rectangle.rs - insert_sibling_after - impl Rectangle - Implement Display trait for Rectangle +src/shapes/rectangle.rs +insert_before + +struct Rectangle { + +Add an import for the `std::fmt` module - src/rectangle.rs - append_child - #imports - Add use statement for fmt +src/shapes/rectangle.rs +insert_after + + Rectangle { width, height } + } +} + +Add a Display implementation for Rectangle + + + +src/shapes/circle.rs +insert_before + +struct Circle { + +Add an import for the `std::fmt` module + + + +src/shapes/circle.rs +insert_after + + Circle { radius } + } +} + +Add a Display implementation for Circle - - - + + ```rs src/user.rs struct User { pub name: String, @@ -202,10 +271,14 @@ impl User { ``` - src/user.rs - update - impl User pub fn print_info - Print all the user information +src/user.rs +update + + pub fn print_info(&self) { + todo!() + } + +Print all the user information @@ -213,442 +286,25 @@ impl User { Remove the 'email' field from the User struct - src/user.rs - delete - struct User email +src/user.rs +delete + +email: String, + - src/user.rs - update - impl User fn new - Remove email parameter from new method - - - - - - - - - - -```rs src/vehicle.rs -struct Vehicle { - make: String, - model: String, - year: u32, +src/user.rs +update + +fn new(name: String, age: u32, email: String) -> Self { + User { name, age, email } } - -impl Vehicle { - fn new(make: String, model: String, year: u32) -> Self { - Vehicle { make, model, year } - } - - fn print_year(&self) { - println!("Year: {}", self.year); - } -} -``` - -Let's add a `start_engine` method for the vehicle and print how long it takes to start it. - - - - -Add a 'use std::time::Instant;' statement at the beginning of the file - - - src/vehicle.rs - prepend_child - #imports - Add use statement for std::time::Instant - - - - -Add a new method 'start_engine' in the Vehicle impl block - -```rust -impl Vehicle { - // ... existing methods ... - - fn start_engine(&self) { - let t0 = Instant::now(); - println!("Starting the engine of {} {} {}. Took: {:?}", - self.year, self.make, self.model, t0.elapsed()); - } -} -``` - - - src/vehicle.rs - append_child - impl Vehicle - Add start_engine method + +Remove email parameter from new method - - - - - - -```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; - } -} -``` - -Let's turn the salary field into an f32. Also, let's delete the department field. - - - - -Make salary an f32 - -```rust -struct Employee { - name: String, - position: String, - salary: f32, - department: String, -} - -impl Employee { - fn new(name: String, position: String, salary: f32, department: String) -> Self { - Employee { name, position, salary, department } - } - - // ... other methods ... - - fn give_raise(&mut self, amount: f32) { - self.salary += amount; - } -} -``` - - - src/employee.rs - update - struct Employee salary - Change salary type to f32 - - - - src/employee.rs - update - impl Employee fn new - Update new method to use f32 for salary - - - - src/employee.rs - update - impl Employee fn give_raise - Update give_raise method to use f32 for amount - - - - -Remove the 'department' field and update the 'print_details' method - -```rust -struct Employee { - name: String, - position: String, - salary: f32, -} - -impl Employee { - fn new(name: String, position: String, salary: f32) -> Self { - Employee { name, position, salary } - } - - fn print_details(&self) { - println!("Name: {}, Position: {}, Salary: {}", - self.name, self.position, self.salary); - } - - // ... other methods ... -} -``` - - - src/employee.rs - delete - struct Employee department - - - - src/employee.rs - update - impl Employee fn new - Remove department parameter from new method - - - - src/employee.rs - update - impl Employee fn print_details - Remove department from 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() } - } -} -``` - -Introduce a level field. - - - - -Add a 'level' field to Player and update the 'new' method - -```rust -struct Player { - name: String, - health: i32, - pub score: u32, - level: u8, -} - -impl Player { - pub fn new(name: String) -> Self { - Player { name, health: 100, score: 0, level: 1 } - } -} -``` - - - src/game.rs - insert_sibling_after - struct Player pub score - Add level field to Player struct - - - - src/game.rs - update - impl Player pub fn new - Initialize level in new method - - - - - - - - - -```rs src/config.rs -use std::collections::HashMap; - -struct Config { - settings: HashMap, -} - -impl Config { - fn new() -> Self { - Config { settings: HashMap::new() } - } -} -``` - -I want to load the configuration from a file. - - - - -Add a 'load_from_file' method to Config and import necessary modules - -```rust -use std::collections::HashMap; -use std::fs::File; -use std::io::{self, BufRead}; -use std::path::Path; - -struct Config { - settings: HashMap, -} - -impl Config { - fn new() -> Self { - Config { settings: HashMap::new() } - } - - fn load_from_file>(&mut self, path: P) -> io::Result<()> { - let file = File::open(path)?; - for line in io::BufReader::new(file).lines() { - let line = line?; - if let Some((key, value)) = line.split_once('=') { - self.settings.insert(key.trim().to_string(), value.trim().to_string()); - } - } - Ok(()) - } -} -``` - - - src/config.rs - append_child - #imports - Import necessary modules for file operations - - - - src/config.rs - append_child - impl Config - Add load_from_file method - - - - - - - - - -```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, &[]) - } -} -``` - -I want to add error handling to this module. - - - - -Add error handling to the 'query' method and create a custom error type - -```rust -use std::fmt; -use std::error::Error as StdError; - -#[derive(Debug)] -pub enum DatabaseError { - ConnectionError(String), - QueryError(String), -} - -impl fmt::Display for DatabaseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - DatabaseError::ConnectionError(msg) => write!(f, "Connection error: {}", msg), - DatabaseError::QueryError(msg) => write!(f, "Query error: {}", msg), - } - } -} - -impl StdError for DatabaseError {} - -// ...omitted code... - -impl Database { - fn new(url: &str) -> Result { - let connection = Connection::connect(url) - .map_err(|e| DatabaseError::ConnectionError(e.to_string()))?; - Ok(Database { connection }) - } - - async fn query(&self, sql: &str) -> Result, DatabaseError> { - self.connection.query(sql, &[]) - .await - .map_err(|e| DatabaseError::QueryError(e.to_string())) - } -} -``` - - - src/database.rs - prepend_child - #imports - Import necessary error handling modules - - - - src/database.rs - insert_sibling_before - pub(crate) struct Database - Define custom DatabaseError enum - - - - src/database.rs - update - impl Database fn new - Update new method to use DatabaseError - - - - src/database.rs - update - impl Database async fn query - Update query method to use DatabaseError - - - - 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/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index f0cd01c4eb..9a0c985d46 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -472,7 +472,7 @@ pub enum XmlTagKind { Step, Edit, Path, - Symbol, + Search, Within, Operation, Description, @@ -1518,7 +1518,7 @@ impl Context { if tag.kind == XmlTagKind::Edit && tag.is_open_tag { let mut path = None; - let mut symbol = None; + let mut search = None; let mut operation = None; let mut description = None; @@ -1527,7 +1527,7 @@ impl Context { edits.push(WorkflowStepEdit::new( path, operation, - symbol, + search, description, )); break; @@ -1536,7 +1536,7 @@ impl Context { if tag.is_open_tag && [ XmlTagKind::Path, - XmlTagKind::Symbol, + XmlTagKind::Search, XmlTagKind::Operation, XmlTagKind::Description, ] @@ -1555,8 +1555,8 @@ impl Context { match kind { XmlTagKind::Path => path = Some(content), XmlTagKind::Operation => operation = Some(content), - XmlTagKind::Symbol => { - symbol = Some(content).filter(|s| !s.is_empty()) + XmlTagKind::Search => { + search = Some(content).filter(|s| !s.is_empty()) } XmlTagKind::Description => { description = diff --git a/crates/assistant/src/context/context_tests.rs b/crates/assistant/src/context/context_tests.rs index c80e105a44..c851ca7438 100644 --- a/crates/assistant/src/context/context_tests.rs +++ b/crates/assistant/src/context/context_tests.rs @@ -609,8 +609,8 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) { « src/lib.rs - insert_sibling_after - fn one + insert_after + fn one add a `two` function @@ -634,8 +634,8 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) { src/lib.rs - insert_sibling_after - fn one + insert_after + fn one add a `two` function » @@ -643,8 +643,8 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) { also,", &[&[WorkflowStepEdit { path: "src/lib.rs".into(), - kind: WorkflowStepEditKind::InsertSiblingAfter { - symbol: "fn one".into(), + kind: WorkflowStepEditKind::InsertAfter { + search: "fn one".into(), description: "add a `two` function".into(), }, }]], @@ -668,8 +668,8 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) { src/lib.rs - insert_sibling_after - «fn zero» + insert_after + «fn zero» add a `two` function @@ -693,8 +693,8 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) { src/lib.rs - insert_sibling_after - fn zero + insert_after + fn zero add a `two` function » @@ -702,8 +702,8 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) { also,", &[&[WorkflowStepEdit { path: "src/lib.rs".into(), - kind: WorkflowStepEditKind::InsertSiblingAfter { - symbol: "fn zero".into(), + kind: WorkflowStepEditKind::InsertAfter { + search: "fn zero".into(), description: "add a `two` function".into(), }, }]], @@ -731,8 +731,8 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) { src/lib.rs - insert_sibling_after - fn zero + insert_after + fn zero add a `two` function @@ -762,8 +762,8 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) { src/lib.rs - insert_sibling_after - fn zero + insert_after + fn zero add a `two` function » @@ -771,8 +771,8 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) { also,", &[&[WorkflowStepEdit { path: "src/lib.rs".into(), - kind: WorkflowStepEditKind::InsertSiblingAfter { - symbol: "fn zero".into(), + kind: WorkflowStepEditKind::InsertAfter { + search: "fn zero".into(), description: "add a `two` function".into(), }, }]], @@ -808,8 +808,8 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) { src/lib.rs - insert_sibling_after - fn zero + insert_after + fn zero add a `two` function » @@ -817,8 +817,8 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) { also,", &[&[WorkflowStepEdit { path: "src/lib.rs".into(), - kind: WorkflowStepEditKind::InsertSiblingAfter { - symbol: "fn zero".into(), + kind: WorkflowStepEditKind::InsertAfter { + search: "fn zero".into(), description: "add a `two` function".into(), }, }]], diff --git a/crates/assistant/src/workflow.rs b/crates/assistant/src/workflow.rs index a99eff366c..75c65ed0a7 100644 --- a/crates/assistant/src/workflow.rs +++ b/crates/assistant/src/workflow.rs @@ -4,16 +4,14 @@ use collections::HashMap; use editor::Editor; use gpui::AsyncAppContext; use gpui::{Model, Task, UpdateGlobal as _, View, WeakView, WindowContext}; -use language::{Anchor, Buffer, BufferSnapshot, Outline, OutlineItem, ParseStatus, SymbolPath}; +use language::{Buffer, BufferSnapshot}; use project::{Project, ProjectPath}; -use rope::Point; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{ops::Range, path::Path, sync::Arc}; +use text::Bias; use workspace::Workspace; -const IMPORTS_SYMBOL: &str = "#imports"; - #[derive(Debug)] pub(crate) struct WorkflowStep { pub range: Range, @@ -45,35 +43,21 @@ pub struct WorkflowSuggestionGroup { #[derive(Clone, Debug, Eq, PartialEq)] pub enum WorkflowSuggestion { Update { - symbol_path: SymbolPath, range: Range, description: String, }, CreateFile { description: String, }, - InsertSiblingBefore { - symbol_path: SymbolPath, + InsertBefore { position: language::Anchor, description: String, }, - InsertSiblingAfter { - symbol_path: SymbolPath, - position: language::Anchor, - description: String, - }, - PrependChild { - symbol_path: Option, - position: language::Anchor, - description: String, - }, - AppendChild { - symbol_path: Option, + InsertAfter { position: language::Anchor, description: String, }, Delete { - symbol_path: SymbolPath, range: Range, }, } @@ -83,10 +67,9 @@ impl WorkflowSuggestion { match self { Self::Update { range, .. } => range.clone(), Self::CreateFile { .. } => language::Anchor::MIN..language::Anchor::MAX, - Self::InsertSiblingBefore { position, .. } - | Self::InsertSiblingAfter { position, .. } - | Self::PrependChild { position, .. } - | Self::AppendChild { position, .. } => *position..*position, + Self::InsertBefore { position, .. } | Self::InsertAfter { position, .. } => { + *position..*position + } Self::Delete { range, .. } => range.clone(), } } @@ -95,10 +78,8 @@ impl WorkflowSuggestion { match self { Self::Update { description, .. } | Self::CreateFile { description } - | Self::InsertSiblingBefore { description, .. } - | Self::InsertSiblingAfter { description, .. } - | Self::PrependChild { description, .. } - | Self::AppendChild { description, .. } => Some(description), + | Self::InsertBefore { description, .. } + | Self::InsertAfter { description, .. } => Some(description), Self::Delete { .. } => None, } } @@ -107,10 +88,8 @@ impl WorkflowSuggestion { match self { Self::Update { description, .. } | Self::CreateFile { description } - | Self::InsertSiblingBefore { description, .. } - | Self::InsertSiblingAfter { description, .. } - | Self::PrependChild { description, .. } - | Self::AppendChild { description, .. } => Some(description), + | Self::InsertBefore { description, .. } + | Self::InsertAfter { description, .. } => Some(description), Self::Delete { .. } => None, } } @@ -161,7 +140,7 @@ impl WorkflowSuggestion { initial_prompt = description.clone(); suggestion_range = editor::Anchor::min()..editor::Anchor::min(); } - Self::InsertSiblingBefore { + Self::InsertBefore { position, description, .. @@ -178,7 +157,7 @@ impl WorkflowSuggestion { line_start..line_start }); } - Self::InsertSiblingAfter { + Self::InsertAfter { position, description, .. @@ -195,40 +174,6 @@ impl WorkflowSuggestion { line_start..line_start }); } - Self::PrependChild { - 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, false, true, cx); - initial_transaction_id = buffer.end_transaction(cx); - buffer.refresh_preview(cx); - - let line_start = buffer.read(cx).anchor_before(line_start); - line_start..line_start - }); - } - Self::AppendChild { - 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, false, cx); - initial_transaction_id = buffer.end_transaction(cx); - buffer.refresh_preview(cx); - - let line_start = buffer.read(cx).anchor_before(line_start); - line_start..line_start - }); - } Self::Delete { range, .. } => { initial_prompt = "Delete".to_string(); suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)? @@ -254,7 +199,7 @@ impl WorkflowStepEdit { pub fn new( path: Option, operation: Option, - symbol: Option, + search: Option, description: Option, ) -> Result { let path = path.ok_or_else(|| anyhow!("missing path"))?; @@ -262,27 +207,19 @@ impl WorkflowStepEdit { let kind = match operation.as_str() { "update" => WorkflowStepEditKind::Update { - symbol: symbol.ok_or_else(|| anyhow!("missing symbol"))?, + search: search.ok_or_else(|| anyhow!("missing search"))?, description: description.ok_or_else(|| anyhow!("missing description"))?, }, - "insert_sibling_before" => WorkflowStepEditKind::InsertSiblingBefore { - symbol: symbol.ok_or_else(|| anyhow!("missing symbol"))?, + "insert_before" => WorkflowStepEditKind::InsertBefore { + search: search.ok_or_else(|| anyhow!("missing search"))?, description: description.ok_or_else(|| anyhow!("missing description"))?, }, - "insert_sibling_after" => WorkflowStepEditKind::InsertSiblingAfter { - symbol: symbol.ok_or_else(|| anyhow!("missing symbol"))?, - description: description.ok_or_else(|| anyhow!("missing description"))?, - }, - "prepend_child" => WorkflowStepEditKind::PrependChild { - symbol, - description: description.ok_or_else(|| anyhow!("missing description"))?, - }, - "append_child" => WorkflowStepEditKind::AppendChild { - symbol, + "insert_after" => WorkflowStepEditKind::InsertAfter { + search: search.ok_or_else(|| anyhow!("missing search"))?, description: description.ok_or_else(|| anyhow!("missing description"))?, }, "delete" => WorkflowStepEditKind::Delete { - symbol: symbol.ok_or_else(|| anyhow!("missing symbol"))?, + search: search.ok_or_else(|| anyhow!("missing search"))?, }, "create" => WorkflowStepEditKind::Create { description: description.ok_or_else(|| anyhow!("missing description"))?, @@ -323,200 +260,143 @@ impl WorkflowStepEdit { })?? .await?; - let mut parse_status = buffer.read_with(&cx, |buffer, _cx| buffer.parse_status())?; - while *parse_status.borrow() != ParseStatus::Idle { - parse_status.changed().await?; - } - let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?; - let outline = snapshot.outline(None).context("no outline for buffer")?; - - let suggestion = match kind { - WorkflowStepEditKind::Update { - symbol, - description, - } => { - let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?; - let start = symbol - .annotation_range - .map_or(symbol.range.start, |range| range.start); - let start = Point::new(start.row, 0); - let end = Point::new( - symbol.range.end.row, - snapshot.line_len(symbol.range.end.row), - ); - let range = snapshot.anchor_before(start)..snapshot.anchor_after(end); - WorkflowSuggestion::Update { - range, - description, - symbol_path, - } - } - WorkflowStepEditKind::Create { description } => { - WorkflowSuggestion::CreateFile { description } - } - WorkflowStepEditKind::InsertSiblingBefore { - symbol, - description, - } => { - let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?; - let position = snapshot.anchor_before( - symbol - .annotation_range - .map_or(symbol.range.start, |annotation_range| { - annotation_range.start - }), - ); - WorkflowSuggestion::InsertSiblingBefore { - position, - description, - symbol_path, - } - } - WorkflowStepEditKind::InsertSiblingAfter { - symbol, - description, - } => { - let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?; - let position = snapshot.anchor_after(symbol.range.end); - WorkflowSuggestion::InsertSiblingAfter { - position, - description, - symbol_path, - } - } - WorkflowStepEditKind::PrependChild { - symbol, - description, - } => { - if let Some(symbol) = symbol { - let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?; - - let position = snapshot.anchor_after( - symbol - .body_range - .map_or(symbol.range.start, |body_range| body_range.start), - ); - WorkflowSuggestion::PrependChild { - position, + let suggestion = cx + .background_executor() + .spawn(async move { + match kind { + WorkflowStepEditKind::Update { + search, description, - symbol_path: Some(symbol_path), + } => { + let range = Self::resolve_location(&snapshot, &search); + WorkflowSuggestion::Update { range, description } } - } else { - WorkflowSuggestion::PrependChild { - position: language::Anchor::MIN, + WorkflowStepEditKind::Create { description } => { + WorkflowSuggestion::CreateFile { description } + } + WorkflowStepEditKind::InsertBefore { + search, description, - symbol_path: None, + } => { + 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 } } } - } - WorkflowStepEditKind::AppendChild { - symbol, - description, - } => { - if let Some(symbol) = symbol { - let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?; - - let position = snapshot.anchor_before( - symbol - .body_range - .map_or(symbol.range.end, |body_range| body_range.end), - ); - WorkflowSuggestion::AppendChild { - position, - description, - symbol_path: Some(symbol_path), - } - } else { - WorkflowSuggestion::PrependChild { - position: language::Anchor::MAX, - description, - symbol_path: None, - } - } - } - WorkflowStepEditKind::Delete { symbol } => { - let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?; - let start = symbol - .annotation_range - .map_or(symbol.range.start, |range| range.start); - let start = Point::new(start.row, 0); - let end = Point::new( - symbol.range.end.row, - snapshot.line_len(symbol.range.end.row), - ); - let range = snapshot.anchor_before(start)..snapshot.anchor_after(end); - WorkflowSuggestion::Delete { range, symbol_path } - } - }; + }) + .await; Ok((buffer, suggestion)) } - fn resolve_symbol( - snapshot: &BufferSnapshot, - outline: &Outline, - symbol: &str, - ) -> Result<(SymbolPath, OutlineItem)> { - if symbol == IMPORTS_SYMBOL { - let target_row = find_first_non_comment_line(snapshot); - Ok(( - SymbolPath(IMPORTS_SYMBOL.to_string()), - OutlineItem { - range: Point::new(target_row, 0)..Point::new(target_row + 1, 0), - ..Default::default() - }, - )) - } else { - let (symbol_path, symbol) = outline - .find_most_similar(symbol) - .with_context(|| format!("symbol not found: {symbol}"))?; - Ok((symbol_path, symbol.to_point(snapshot))) + 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) } } -fn find_first_non_comment_line(snapshot: &BufferSnapshot) -> u32 { - let Some(language) = snapshot.language() else { - return 0; - }; - - let scope = language.default_scope(); - let comment_prefixes = scope.line_comment_prefixes(); - - let mut chunks = snapshot.as_rope().chunks(); - let mut target_row = 0; - loop { - let starts_with_comment = chunks - .peek() - .map(|chunk| { - comment_prefixes - .iter() - .any(|s| chunk.starts_with(s.as_ref().trim_end())) - }) - .unwrap_or(false); - - if !starts_with_comment { - break; - } - - target_row += 1; - if !chunks.next_line() { - break; - } - } - target_row -} - #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(tag = "operation")] pub enum WorkflowStepEditKind { - /// Rewrites the specified symbol entirely based on the given description. - /// This operation completely replaces the existing symbol with new content. + /// Rewrites the specified text entirely based on the given description. + /// This operation completely replaces the given text. Update { - /// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`. - /// The path should uniquely identify the symbol within the containing file. - symbol: String, + /// 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, }, @@ -526,47 +406,101 @@ pub enum WorkflowStepEditKind { /// A brief description of the file to be created. description: String, }, - /// Inserts a new symbol based on the given description before the specified symbol. - /// This operation adds new content immediately preceding an existing symbol. - InsertSiblingBefore { - /// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`. - /// The new content will be inserted immediately before this symbol. - symbol: String, - /// A brief description of the new symbol to be inserted. + /// 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 a new symbol based on the given description after the specified symbol. - /// This operation adds new content immediately following an existing symbol. - InsertSiblingAfter { - /// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`. - /// The new content will be inserted immediately after this symbol. - symbol: String, - /// A brief description of the new symbol to be inserted. - description: String, - }, - /// Inserts a new symbol as a child of the specified symbol at the start. - /// This operation adds new content as the first child of an existing symbol (or file if no symbol is provided). - PrependChild { - /// An optional fully-qualified reference to the symbol after the code you want to insert, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`. - /// If provided, the new content will be inserted as the first child of this symbol. - /// If not provided, the new content will be inserted at the top of the file. - symbol: Option, - /// A brief description of the new symbol to be inserted. - description: String, - }, - /// Inserts a new symbol as a child of the specified symbol at the end. - /// This operation adds new content as the last child of an existing symbol (or file if no symbol is provided). - AppendChild { - /// An optional fully-qualified reference to the symbol before the code you want to insert, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`. - /// If provided, the new content will be inserted as the last child of this symbol. - /// If not provided, the new content will be applied at the bottom of the file. - symbol: Option, - /// A brief description of the new symbol to be inserted. + /// 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 { - /// An fully-qualified reference to the symbol to be deleted, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`. - symbol: String, + /// 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) + ); + } + } +}