Have models indicate code locations in workflows using textual search, not symbol names (#17282)

Max Brunsfeld and Antonio Scandurra created

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>

Change summary

assets/prompts/edit_workflow.hbs              | 632 ++++----------------
crates/assistant/src/context.rs               |  12 
crates/assistant/src/context/context_tests.rs |  44 
crates/assistant/src/workflow.rs              | 508 +++++++---------
4 files changed, 393 insertions(+), 803 deletions(-)

Detailed changes

assets/prompts/edit_workflow.hbs 🔗

@@ -27,17 +27,17 @@ impl Person {
 ```
 
 <edit>
-  <path>src/person.rs</path>
-  <operation>insert_before</operation>
-  <symbol>struct Person height</symbol>
-  <description>Add the age field</description>
+<path>src/person.rs</path>
+<operation>insert_before</operation>
+<search>height: f32,</search>
+<description>Add the age field</description>
 </edit>
 
 <edit>
-  <path>src/person.rs</path>
-  <operation>append_child</operation>
-  <symbol>impl Person</symbol>
-  <description>Add the age getter</description>
+<path>src/person.rs</path>
+<operation>insert_after</operation>
+<search>impl Person {</search>
+<description>Add the age getter</description>
 </edit>
 </step>
 
@@ -45,15 +45,15 @@ impl Person {
 
 First, each `<step>` 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 `<step>` must contain one or more `<edit>` tags, each of which refer to a specific range in a source file. Each `<edit>` tag must contain the following child tags:
+After the description, each `<step>` must contain one or more `<edit>` tags, each of which refer to a specific range in a source file. Each `<edit>` tag must contain the following child tags:
 
 ### `<path>` (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.
 
-### `<symbol>` (optional)
+### `<search>` (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.
 
 ### `<description>` (required)
 
@@ -62,593 +62,249 @@ This tag contains a single-line description of the edit that should be made at t
 ### `<operation>` (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.
 
 <guidelines>
 - 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.
 </guidelines>
 
 Here are some concrete examples.
 
-<example language="rust">
-
+<example>
 <message role="user">
-```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?
-</message>
-
-<message role="assistant">
-<step>
-Add methods to calculate the area and perimeter of the rectangle
-
-```rust
-impl Rectangle {
-    // existing methods...
-
-    fn calculate_area(&self) -> f64 {
-        self.width * self.height
-    }
-
-    fn calculate_perimeter(&self) -> f64 {
-        2.0 * (self.width + self.height)
-    }
+```rs src/shapes/circle.rs
+pub struct Circle {
+    radius: f64,
 }
-```
-
-<edit>
-  <path>src/rectangle.rs</path>
-  <operation>append_child</operation>
-  <symbol>impl Rectangle</symbol>
-  <description>Add calculate_area and calculate_perimeter methods</description>
-</edit>
-</step>
-
-<step>
-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)
+impl Circle {
+    pub fn new(radius: f64) -> Self {
+        Circle { radius }
     }
 }
 ```
 
-<edit>
-  <path>src/rectangle.rs</path>
-  <operation>insert_sibling_after</operation>
-  <symbol>impl Rectangle</symbol>
-  <description>Implement Display trait for Rectangle</description>
-</edit>
-
-<edit>
-  <path>src/rectangle.rs</path>
-  <operation>append_child</operation>
-  <symbol>#imports</symbol>
-  <description>Add use statement for fmt</description>
-</edit>
-</step>
+Update all shapes to store their origin as an (x, y) tuple and implement Display.
 </message>
 
-</example>
-
-<example language="rust">
-
-<message role="user">
-```rs src/user.rs
-struct User {
-    pub name: String,
-    age: u32,
-    email: String,
-}
+<message role="assistant">
+We'll need to update both the rectangle and circle modules.
 
-impl User {
-    fn new(name: String, age: u32, email: String) -> Self {
-        User { name, age, email }
-    }
+<step>
+Add origin fields to both shape types.
 
-    pub fn print_info(&self) {
-        todo!()
-    }
+```rust
+struct Rectangle {
+    // existing fields ...
+    origin: (f64, f64),
 }
 ```
 
-Let's print all the user information and delete the email field.
-</message>
-
-<message role="assistant">
-<step>
-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);
-    }
+struct Circle {
+    // existing fields ...
+    origin: (f64, f64),
 }
 ```
 
 <edit>
-  <path>src/user.rs</path>
-  <operation>update</operation>
-  <symbol>impl User pub fn print_info</symbol>
-  <description>Print all the user information</description>
-</edit>
-</step>
-
-<step>
-Remove the 'email' field from the User struct
-
-<edit>
-  <path>src/user.rs</path>
-  <operation>delete</operation>
-  <symbol>struct User email</symbol>
+<path>src/shapes/rectangle.rs</path>
+<operation>insert_before</operation>
+<search>
+    width: f64,
+    height: f64,
+</search>
+<description>Add the origin field to Rectangle</description>
 </edit>
 
 <edit>
-  <path>src/user.rs</path>
-  <operation>update</operation>
-  <symbol>impl User fn new</symbol>
-  <description>Remove email parameter from new method</description>
+<path>src/shapes/circle.rs</path>
+<operation>insert_before</operation>
+<search>
+    radius: f64,
+</search>
+<description>Add the origin field to Circle</description>
 </edit>
 
-</step>
-</message>
-
-</example>
-
-<example language="rust">
-
-<message role="user">
-```rs src/vehicle.rs
-struct Vehicle {
-    make: String,
-    model: String,
-    year: u32,
-}
-
-impl Vehicle {
-    fn new(make: String, model: String, year: u32) -> Self {
-        Vehicle { make, model, year }
-    }
-
-    fn print_year(&self) {
-        println!("Year: {}", self.year);
-    }
-}
-```
-
-Let's add a `start_engine` method for the vehicle and print how long it takes to start it.
-</message>
-
-<message role="assistant">
 <step>
-Add a 'use std::time::Instant;' statement at the beginning of the file
+Update both shape's constructors to take an origin.
 
 <edit>
-  <path>src/vehicle.rs</path>
-  <operation>prepend_child</operation>
-  <symbol>#imports</symbol>
-  <description>Add use statement for std::time::Instant</description>
-</edit>
-</step>
-
-<step>
-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());
+<path>src/shapes/rectangle.rs</path>
+<operation>update</operation>
+<search>
+    fn new(width: f64, height: f64) -> Self {
+        Rectangle { width, height }
     }
-}
-```
+</search>
+<description>Update the Rectangle new function to take an origin</description>
+</edit>
 
 <edit>
-  <path>src/vehicle.rs</path>
-  <operation>append_child</operation>
-  <symbol>impl Vehicle</symbol>
-  <description>Add start_engine method</description>
+<path>src/shapes/circle.rs</path>
+<operation>update</operation>
+<search>
+    fn new(radius: f64) -> Self {
+        Circle { radius }
+    }
+</search>
+<description>Update the Circle new function to take an origin</description>
 </edit>
 </step>
-</message>
-
-</example>
-
-<example language="rust">
-
-<message role="user">
-```rs src/employee.rs
-struct Employee {
-    name: String,
-    position: String,
-    salary: u32,
-    department: String,
-}
 
-impl Employee {
-    fn new(name: String, position: String, salary: u32, department: String) -> Self {
-        Employee { name, position, salary, department }
-    }
-
-    fn print_details(&self) {
-        println!("Name: {}, Position: {}, Salary: {}, Department: {}",
-                  self.name, self.position, self.salary, self.department);
-    }
-
-    fn give_raise(&mut self, amount: u32) {
-        self.salary += amount;
-    }
-}
-```
-
-Let's turn the salary field into an f32. Also, let's delete the department field.
-</message>
-
-<message role="assistant">
 <step>
-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;
-    }
-}
-```
-
-<edit>
-  <path>src/employee.rs</path>
-  <operation>update</operation>
-  <symbol>struct Employee salary</symbol>
-  <description>Change salary type to f32</description>
-</edit>
+Implement Display for both shapes
 
 <edit>
-  <path>src/employee.rs</path>
-  <operation>update</operation>
-  <symbol>impl Employee fn new</symbol>
-  <description>Update new method to use f32 for salary</description>
+<path>src/shapes/rectangle.rs</path>
+<operation>insert_before</operation>
+<search>
+struct Rectangle {
+</search>
+<description>Add an import for the `std::fmt` module</description>
 </edit>
 
 <edit>
-  <path>src/employee.rs</path>
-  <operation>update</operation>
-  <symbol>impl Employee fn give_raise</symbol>
-  <description>Update give_raise method to use f32 for amount</description>
-</edit>
-</step>
-
-<step>
-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);
+<path>src/shapes/rectangle.rs</path>
+<operation>insert_after</operation>
+<search>
+        Rectangle { width, height }
     }
-
-    // ... other methods ...
 }
-```
-
-<edit>
-  <path>src/employee.rs</path>
-  <operation>delete</operation>
-  <symbol>struct Employee department</symbol>
+</search>
+<description>Add a Display implementation for Rectangle</description>
 </edit>
 
 <edit>
-  <path>src/employee.rs</path>
-  <operation>update</operation>
-  <symbol>impl Employee fn new</symbol>
-  <description>Remove department parameter from new method</description>
+<path>src/shapes/circle.rs</path>
+<operation>insert_before</operation>
+<search>
+struct Circle {
+</search>
+<description>Add an import for the `std::fmt` module</description>
 </edit>
 
 <edit>
-  <path>src/employee.rs</path>
-  <operation>update</operation>
-  <symbol>impl Employee fn print_details</symbol>
-  <description>Remove department from print_details method</description>
-</edit>
-</step>
-</message>
-
-</example>
-
-<example language="rust">
-
-<message role="user">
-```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<Player>,
-}
-
-impl Game {
-    fn new() -> Self {
-        Game { players: Vec::new() }
-    }
-}
-```
-
-Introduce a level field.
-</message>
-
-<message role="assistant">
-<step>
-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 }
+<path>src/shapes/circle.rs</path>
+<operation>insert_after</operation>
+<search>
+        Circle { radius }
     }
 }
-```
-
-<edit>
-  <path>src/game.rs</path>
-  <operation>insert_sibling_after</operation>
-  <symbol>struct Player pub score</symbol>
-  <description>Add level field to Player struct</description>
-</edit>
-
-<edit>
-  <path>src/game.rs</path>
-  <operation>update</operation>
-  <symbol>impl Player pub fn new</symbol>
-  <description>Initialize level in new method</description>
+</search>
+<description>Add a Display implementation for Circle</description>
 </edit>
 </step>
 </message>
-
 </example>
 
-<example language="rust">
-
+<example number=2>
 <message role="user">
-```rs src/config.rs
-use std::collections::HashMap;
-
-struct Config {
-    settings: HashMap<String, String>,
-}
-
-impl Config {
-    fn new() -> Self {
-        Config { settings: HashMap::new() }
-    }
-}
-```
-
-I want to load the configuration from a file.
-</message>
-
-<message role="assistant">
-<step>
-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<String, String>,
-}
-
-impl Config {
-    fn new() -> Self {
-        Config { settings: HashMap::new() }
-    }
-
-    fn load_from_file<P: AsRef<Path>>(&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(())
-    }
-}
-```
-
-<edit>
-  <path>src/config.rs</path>
-  <operation>append_child</operation>
-  <symbol>#imports</symbol>
-  <description>Import necessary modules for file operations</description>
-</edit>
-
-<edit>
-  <path>src/config.rs</path>
-  <operation>append_child</operation>
-  <symbol>impl Config</symbol>
-  <description>Add load_from_file method</description>
-</edit>
-</step>
-</message>
-
-</example>
 
-<example language="rust">
-
-<message role="user">
-```rs src/database.rs
-pub(crate) struct Database {
-    connection: Connection,
+```rs src/user.rs
+struct User {
+    pub name: String,
+    age: u32,
+    email: String,
 }
 
-impl Database {
-    fn new(url: &str) -> Result<Self, Error> {
-        let connection = Connection::connect(url)?;
-        Ok(Database { connection })
+impl User {
+    fn new(name: String, age: u32, email: String) -> Self {
+        User { name, age, email }
     }
 
-    async fn query(&self, sql: &str) -> Result<Vec<Row>, Error> {
-        self.connection.query(sql, &[])
+    pub fn print_info(&self) {
+        todo!()
     }
 }
 ```
 
-I want to add error handling to this module.
+Let's print all the user information and delete the email field.
 </message>
 
 <message role="assistant">
 <step>
-Add error handling to the 'query' method and create a custom error type
+Update the 'print_info' method to print user information
 
 ```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<Self, DatabaseError> {
-        let connection = Connection::connect(url)
-            .map_err(|e| DatabaseError::ConnectionError(e.to_string()))?;
-        Ok(Database { connection })
-    }
+impl User {
+    // ... other methods ...
 
-    async fn query(&self, sql: &str) -> Result<Vec<Row>, DatabaseError> {
-        self.connection.query(sql, &[])
-            .await
-            .map_err(|e| DatabaseError::QueryError(e.to_string()))
+    pub fn print_info(&self) {
+        println!("Name: {name}, Age: {age}", name = self.name, age = self.age);
     }
 }
 ```
 
 <edit>
-  <path>src/database.rs</path>
-  <operation>prepend_child</operation>
-  <symbol>#imports</symbol>
-  <description>Import necessary error handling modules</description>
+<path>src/user.rs</path>
+<operation>update</operation>
+<search>
+    pub fn print_info(&self) {
+        todo!()
+    }
+</search>
+<description>Print all the user information</description>
 </edit>
+</step>
 
-<edit>
-  <path>src/database.rs</path>
-  <operation>insert_sibling_before</operation>
-  <symbol>pub(crate) struct Database</symbol>
-  <description>Define custom DatabaseError enum</description>
-</edit>
+<step>
+Remove the 'email' field from the User struct
 
 <edit>
-  <path>src/database.rs</path>
-  <operation>update</operation>
-  <symbol>impl Database fn new</symbol>
-  <description>Update new method to use DatabaseError</description>
+<path>src/user.rs</path>
+<operation>delete</operation>
+<search>
+email: String,
+</search>
 </edit>
 
 <edit>
-  <path>src/database.rs</path>
-  <operation>update</operation>
-  <symbol>impl Database async fn query</symbol>
-  <description>Update query method to use DatabaseError</description>
+<path>src/user.rs</path>
+<operation>update</operation>
+<symbol>
+fn new(name: String, age: u32, email: String) -> Self {
+    User { name, age, email }
+}
+</symbol>
+<description>Remove email parameter from new method</description>
 </edit>
 </step>
 </message>
-
 </example>
 
 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.

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 =

crates/assistant/src/context/context_tests.rs 🔗

@@ -609,8 +609,8 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
 
         <edit>«
         <path>src/lib.rs</path>
-        <operation>insert_sibling_after</operation>
-        <symbol>fn one</symbol>
+        <operation>insert_after</operation>
+        <search>fn one</search>
         <description>add a `two` function</description>
         </edit>
         </step>
@@ -634,8 +634,8 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
 
         <edit>
         <path>src/lib.rs</path>
-        <operation>insert_sibling_after</operation>
-        <symbol>fn one</symbol>
+        <operation>insert_after</operation>
+        <search>fn one</search>
         <description>add a `two` function</description>
         </edit>
         </step>»
@@ -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) {
 
         <edit>
         <path>src/lib.rs</path>
-        <operation>insert_sibling_after</operation>
-        <symbol>«fn zero»</symbol>
+        <operation>insert_after</operation>
+        <search>«fn zero»</search>
         <description>add a `two` function</description>
         </edit>
         </step>
@@ -693,8 +693,8 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
 
         <edit>
         <path>src/lib.rs</path>
-        <operation>insert_sibling_after</operation>
-        <symbol>fn zero</symbol>
+        <operation>insert_after</operation>
+        <search>fn zero</search>
         <description>add a `two` function</description>
         </edit>
         </step>»
@@ -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) {
 
         <edit>
         <path>src/lib.rs</path>
-        <operation>insert_sibling_after</operation>
-        <symbol>fn zero</symbol>
+        <operation>insert_after</operation>
+        <search>fn zero</search>
         <description>add a `two` function</description>
         </edit>
         </step>
@@ -762,8 +762,8 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
 
         <edit>
         <path>src/lib.rs</path>
-        <operation>insert_sibling_after</operation>
-        <symbol>fn zero</symbol>
+        <operation>insert_after</operation>
+        <search>fn zero</search>
         <description>add a `two` function</description>
         </edit>
         </step>»
@@ -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) {
 
         <edit>
         <path>src/lib.rs</path>
-        <operation>insert_sibling_after</operation>
-        <symbol>fn zero</symbol>
+        <operation>insert_after</operation>
+        <search>fn zero</search>
         <description>add a `two` function</description>
         </edit>
         </step>»
@@ -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(),
             },
         }]],

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<language::Anchor>,
@@ -45,35 +43,21 @@ pub struct WorkflowSuggestionGroup {
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub enum WorkflowSuggestion {
     Update {
-        symbol_path: SymbolPath,
         range: Range<language::Anchor>,
         description: String,
     },
     CreateFile {
         description: String,
     },
-    InsertSiblingBefore {
-        symbol_path: SymbolPath,
-        position: language::Anchor,
-        description: String,
-    },
-    InsertSiblingAfter {
-        symbol_path: SymbolPath,
-        position: language::Anchor,
-        description: String,
-    },
-    PrependChild {
-        symbol_path: Option<SymbolPath>,
+    InsertBefore {
         position: language::Anchor,
         description: String,
     },
-    AppendChild {
-        symbol_path: Option<SymbolPath>,
+    InsertAfter {
         position: language::Anchor,
         description: String,
     },
     Delete {
-        symbol_path: SymbolPath,
         range: Range<language::Anchor>,
     },
 }
@@ -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<String>,
         operation: Option<String>,
-        symbol: Option<String>,
+        search: Option<String>,
         description: Option<String>,
     ) -> Result<Self> {
         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"))?,
-                description: description.ok_or_else(|| anyhow!("missing description"))?,
-            },
-            "insert_sibling_before" => WorkflowStepEditKind::InsertSiblingBefore {
-                symbol: symbol.ok_or_else(|| anyhow!("missing symbol"))?,
-                description: description.ok_or_else(|| anyhow!("missing description"))?,
-            },
-            "insert_sibling_after" => WorkflowStepEditKind::InsertSiblingAfter {
-                symbol: symbol.ok_or_else(|| anyhow!("missing symbol"))?,
+                search: search.ok_or_else(|| anyhow!("missing search"))?,
                 description: description.ok_or_else(|| anyhow!("missing description"))?,
             },
-            "prepend_child" => WorkflowStepEditKind::PrependChild {
-                symbol,
+            "insert_before" => WorkflowStepEditKind::InsertBefore {
+                search: search.ok_or_else(|| anyhow!("missing search"))?,
                 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,
-                        description,
-                        symbol_path: None,
+                    WorkflowStepEditKind::Create { description } => {
+                        WorkflowSuggestion::CreateFile { description }
                     }
-                }
-            }
-            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,
+                    WorkflowStepEditKind::InsertBefore {
+                        search,
                         description,
-                        symbol_path: Some(symbol_path),
+                    } => {
+                        let range = Self::resolve_location(&snapshot, &search);
+                        WorkflowSuggestion::InsertBefore {
+                            position: range.start,
+                            description,
+                        }
                     }
-                } else {
-                    WorkflowSuggestion::PrependChild {
-                        position: language::Anchor::MAX,
+                    WorkflowStepEditKind::InsertAfter {
+                        search,
                         description,
-                        symbol_path: None,
+                    } => {
+                        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::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<Anchor>,
-        symbol: &str,
-    ) -> Result<(SymbolPath, OutlineItem<Point>)> {
-        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<text::Anchor> {
+        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<f64>,
         }
-    }
-}
 
-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);
+        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);
+            }
+        }
 
-        if !starts_with_comment {
-            break;
+        // 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;
+            }
         }
 
-        target_row += 1;
-        if !chunks.next_line() {
-            break;
+        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)
     }
-    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<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 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<String>,
-        /// 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)
+            );
+        }
+    }
+}