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)
+ );
+ }
+ }
+}