Immediate edit step resolution (#16447)
## Todo * [x] Parse and present new XML output * [x] Resolve new edits to buffers and anchor ranges * [x] Surface resolution errors * [x] Steps fail to resolve because language hasn't loaded yet * [x] Treat empty `<symbol>` tag as None * [x] duplicate assists when editing steps * [x] step footer blocks can appear *below* the following message header block ## Release Notes: - N/A --------- Co-authored-by: Mikayla <mikayla@zed.dev> Co-authored-by: Peter <peter@zed.dev> Co-authored-by: Marshall <marshall@zed.dev> Co-authored-by: Antonio <antonio@zed.dev> Co-authored-by: Antonio Scandurra <me@as-cii.com>
This commit is contained in:
parent
fc4c533d0a
commit
f84ef5e48a
16 changed files with 2737 additions and 2336 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -417,6 +417,7 @@ dependencies = [
|
||||||
"similar",
|
"similar",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
|
"strum",
|
||||||
"telemetry_events",
|
"telemetry_events",
|
||||||
"terminal",
|
"terminal",
|
||||||
"terminal_view",
|
"terminal_view",
|
||||||
|
|
|
@ -1,302 +1,656 @@
|
||||||
<workflow>
|
<task_description>
|
||||||
Guide the user through code changes in numbered steps that focus on individual functions, type definitions, etc.
|
|
||||||
Surround each distinct step in a <step></step> XML tag. The user will be performing these steps in a code editor
|
|
||||||
named Zed, which is where they will have entered this prompt and will be seeing the response.
|
|
||||||
|
|
||||||
<instructions>
|
# Code Change Workflow
|
||||||
- Use the language of the file for code fence blocks unless otherwise specified.
|
|
||||||
- Include a code or file action in each step.
|
|
||||||
- Only put code in separate steps if it should either go in separate files, or in different (non-contiguous) places in the same file.
|
|
||||||
- Provide error handling and input validation where appropriate.
|
|
||||||
- Adapt explanations based on the user's perceived level of expertise.
|
|
||||||
- Include comments in code examples to enhance understanding.
|
|
||||||
- Consider more complex programming scenarios when relevant.
|
|
||||||
- Avoid using the terminal to perform filesystem operations such as creating, deleting, renaming, and editing files. Assume the user prefers to use Zed for these operations, unless the prompt suggests that the user is expecting terminal commands.
|
|
||||||
- When creating a new file, Zed will automatically create any necessary directories in that file's path. So when a step will create a new file in Zed, don't mention needing to create the directory that it will go in.
|
|
||||||
- Assume the user is performing these actions in the Zed code editor, so avoid redundancies like "In your code editor, ..." or "In Zed, ..."
|
|
||||||
- Be concise without leaving out important information.
|
|
||||||
</instructions>
|
|
||||||
|
|
||||||
Examples of user-assistant dialogs with step numbers and user follow-up requests:
|
Your task is to guide the user through code changes using a series of steps. Each step should describe a high-level change, which can consist of multiple edits to distinct locations in the codebase.
|
||||||
|
|
||||||
|
## Output Example
|
||||||
|
|
||||||
|
Provide output as XML, with the following format:
|
||||||
|
|
||||||
|
<step>
|
||||||
|
Update the Person struct to store an age
|
||||||
|
|
||||||
|
```rust
|
||||||
|
struct Person {
|
||||||
|
// existing fields...
|
||||||
|
age: u8,
|
||||||
|
height: f32,
|
||||||
|
// existing fields...
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Person {
|
||||||
|
fn age(&self) -> u8 {
|
||||||
|
self.age
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<edit>
|
||||||
|
<path>src/person.rs</path>
|
||||||
|
<operation>insert_before</operation>
|
||||||
|
<symbol>struct Person height</symbol>
|
||||||
|
<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>
|
||||||
|
</edit>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
### `<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)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### `<description>` (required)
|
||||||
|
|
||||||
|
This tag contains a single-line description of the edit that should be made at the given location.
|
||||||
|
|
||||||
|
### `<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.
|
||||||
|
- `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.
|
||||||
|
|
||||||
|
<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.
|
||||||
|
- 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.
|
||||||
|
- 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"`
|
||||||
|
</guidelines>
|
||||||
|
|
||||||
|
Here are some concrete examples.
|
||||||
|
|
||||||
<example language="rust">
|
<example language="rust">
|
||||||
<turn number="1">
|
|
||||||
<user>Create a Library struct with a vector of Book structs. Include methods to add books and find books by author. Use generics for the book ID.</user>
|
|
||||||
<assistant>Certainly! Let's create a Library struct with a vector of Book structs, including methods to add books and find books by author, using generics for the book ID:
|
|
||||||
|
|
||||||
<step>Define the Book and Library structs
|
<message role="user">
|
||||||
```rust
|
```rs src/rectangle.rs
|
||||||
use std::fmt::Display;
|
struct Rectangle {
|
||||||
|
width: f64,
|
||||||
struct Book<T>
|
height: f64,
|
||||||
where
|
|
||||||
T: Display + Clone
|
|
||||||
{
|
|
||||||
id: T,
|
|
||||||
title: String,
|
|
||||||
author: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Library<T>
|
impl Rectangle {
|
||||||
where
|
fn new(width: f64, height: f64) -> Self {
|
||||||
T: Display + Clone
|
Rectangle { width, height }
|
||||||
{
|
|
||||||
books: Vec<Book<T>>,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</step>
|
|
||||||
<step>Implement methods for the Library struct
|
|
||||||
```rust
|
|
||||||
impl<T> Library<T>
|
|
||||||
where
|
|
||||||
T: Display + Clone
|
|
||||||
{
|
|
||||||
fn new() -> Self {
|
|
||||||
Library { books: Vec::new() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_book(&mut self, id: T, title: String, author: String) {
|
|
||||||
let book = Book { id, title, author };
|
|
||||||
self.books.push(book);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_books_by_author(&self, author: &str) -> Vec<&Book<T>> {
|
|
||||||
self.books
|
|
||||||
.iter()
|
|
||||||
.filter(|book| book.author.to_lowercase() == author.to_lowercase())
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
</step>
|
|
||||||
|
|
||||||
This code defines:
|
We need to add methods to calculate the area and perimeter of the rectangle. Can you help with that?
|
||||||
1. A generic Book struct with an ID of type T, title, and author
|
</message>
|
||||||
2. A Library struct containing a vector of Books
|
|
||||||
3. Methods to create a new Library, add books, and find books by author
|
|
||||||
The use of generics allows flexibility in the type of ID used for books.</assistant>
|
|
||||||
</turn>
|
|
||||||
|
|
||||||
<turn number="2">
|
<message role="assistant">
|
||||||
<user>How can I use this Library struct in a main function?</user>
|
<step>
|
||||||
<assistant>Here's how you can use the Library struct in a main function:
|
Add methods to calculate the area and perimeter of the rectangle
|
||||||
|
|
||||||
<step>Create a main function to demonstrate Library usage
|
|
||||||
```rust
|
```rust
|
||||||
fn main() {
|
impl Rectangle {
|
||||||
// Create a new Library with string IDs
|
// existing methods...
|
||||||
let mut library = Library::new();
|
|
||||||
|
|
||||||
// Add some books
|
fn calculate_area(&self) -> f64 {
|
||||||
library.add_book(String::from("001"), String::from("The Rust Programming Language"), String::from("Steve Klabnik"));
|
self.width * self.height
|
||||||
library.add_book(String::from("002"), String::from("Programming Rust"), String::from("Jim Blandy"));
|
}
|
||||||
library.add_book(String::from("003"), String::from("Rust in Action"), String::from("Tim McNamara"));
|
|
||||||
|
|
||||||
// Find books by author
|
fn calculate_perimeter(&self) -> f64 {
|
||||||
let author = "Steve Klabnik";
|
2.0 * (self.width + self.height)
|
||||||
let books = library.find_books_by_author(author);
|
|
||||||
|
|
||||||
println!("Books by {}:", author);
|
|
||||||
for book in books {
|
|
||||||
println!("ID: {}, Title: {}", book.id, book.title);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
This main function demonstrates:
|
<step>
|
||||||
1. Creating a new Library instance
|
Implement the Display trait for the Rectangle struct
|
||||||
2. Adding books with string IDs
|
|
||||||
3. Finding and displaying books by a specific author</assistant>
|
|
||||||
</turn>
|
|
||||||
|
|
||||||
<turn number="3">
|
|
||||||
<user>Can you add a method to remove a book by ID and handle potential errors?</user>
|
|
||||||
<assistant>Certainly! Let's add a method to remove a book by ID and handle potential errors:
|
|
||||||
|
|
||||||
<step>Add a remove_book method to the Library struct
|
|
||||||
```rust
|
```rust
|
||||||
use std::fmt::Display;
|
|
||||||
use std::error::Error;
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Debug)]
|
impl fmt::Display for Rectangle {
|
||||||
struct BookNotFoundError;
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "Rectangle: {}x{}", self.width, self.height)
|
||||||
impl Error for BookNotFoundError {}
|
|
||||||
|
|
||||||
impl fmt::Display for BookNotFoundError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "Book not found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Library<T>
|
|
||||||
where
|
|
||||||
T: Display + Clone + PartialEq
|
|
||||||
{
|
|
||||||
// ... (previous methods remain the same)
|
|
||||||
|
|
||||||
fn remove_book(&mut self, id: &T) -> Result<Book<T>, BookNotFoundError> {
|
|
||||||
if let Some(index) = self.books.iter().position(|book| &book.id == id) {
|
|
||||||
Ok(self.books.remove(index))
|
|
||||||
} else {
|
|
||||||
Err(BookNotFoundError)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<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>
|
</step>
|
||||||
<step>Update the main function to demonstrate book removal
|
</message>
|
||||||
|
|
||||||
|
</example>
|
||||||
|
|
||||||
|
<example language="rust">
|
||||||
|
|
||||||
|
<message role="user">
|
||||||
|
```rs src/user.rs
|
||||||
|
struct User {
|
||||||
|
pub name: String,
|
||||||
|
age: u32,
|
||||||
|
email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
fn new(name: String, age: u32, email: String) -> Self {
|
||||||
|
User { name, age, email }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_info(&self) {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
```rust
|
||||||
fn main() {
|
impl User {
|
||||||
// ... (previous code remains the same)
|
// ... other methods ...
|
||||||
|
|
||||||
// Attempt to remove a book
|
pub fn print_info(&self) {
|
||||||
let id_to_remove = String::from("002");
|
println!("Name: {name}, Age: {age}", name = self.name, age = self.age);
|
||||||
match library.remove_book(&id_to_remove) {
|
|
||||||
Ok(removed_book) => println!("Removed book: {} by {}", removed_book.title, removed_book.author),
|
|
||||||
Err(e) => println!("Error removing book: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to remove a non-existent book
|
|
||||||
let non_existent_id = String::from("999");
|
|
||||||
match library.remove_book(&non_existent_id) {
|
|
||||||
Ok(_) => println!("Book removed successfully"),
|
|
||||||
Err(e) => println!("Error removing book: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display remaining books
|
|
||||||
println!("\nRemaining books:");
|
|
||||||
for book in &library.books {
|
|
||||||
println!("ID: {}, Title: {} by {}", book.id, book.title, book.author);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
This update includes:
|
<step>
|
||||||
1. A new remove_book method that returns a Result type
|
Remove the 'email' field from the User struct
|
||||||
2. A custom BookNotFoundError for error handling
|
|
||||||
3. Demonstration of successful book removal and error handling for non-existent books
|
<edit>
|
||||||
4. Display of remaining books after removal attempts
|
<path>src/user.rs</path>
|
||||||
|
<operation>delete</operation>
|
||||||
|
<symbol>struct User email</symbol>
|
||||||
|
</edit>
|
||||||
|
|
||||||
|
<edit>
|
||||||
|
<path>src/user.rs</path>
|
||||||
|
<operation>update</operation>
|
||||||
|
<symbol>impl User fn new</symbol>
|
||||||
|
<description>Remove email parameter from new method</description>
|
||||||
|
</edit>
|
||||||
|
|
||||||
|
</step>
|
||||||
|
</message>
|
||||||
|
|
||||||
The remove_book method uses the PartialEq trait to compare book IDs, allowing for flexible ID types while maintaining type safety.</assistant>
|
|
||||||
</turn>
|
|
||||||
</example>
|
</example>
|
||||||
|
|
||||||
<example language="go">
|
<example language="rust">
|
||||||
<turn number="1">
|
|
||||||
<user>Create a Person struct with name, age, and email fields, and a method to greet. Include error handling for age.</user>
|
|
||||||
<assistant>Certainly! Let's create a Person struct with the specified fields and a method to greet in Go:
|
|
||||||
|
|
||||||
<step>Define the Person struct and NewPerson function with error handling
|
<message role="user">
|
||||||
```go
|
```rs src/vehicle.rs
|
||||||
type Person struct {
|
struct Vehicle {
|
||||||
Name string
|
make: String,
|
||||||
Age int
|
model: String,
|
||||||
Email string
|
year: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPerson(name string, age int, email string) (*Person, error) {
|
impl Vehicle {
|
||||||
if age < 0 || age > 150 {
|
fn new(make: String, model: String, year: u32) -> Self {
|
||||||
return nil, fmt.Errorf("invalid age: %d", age)
|
Vehicle { make, model, year }
|
||||||
}
|
|
||||||
return &Person{Name: name, Age: age, Email: email}, nil
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</step>
|
|
||||||
<step>Implement the Greet method for the Person struct
|
|
||||||
```go
|
|
||||||
func (p *Person) Greet() string {
|
|
||||||
return fmt.Sprintf("Hello, I'm %s, %d years old. You can reach me at %s.", p.Name, p.Age, p.Email)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</step>
|
|
||||||
|
|
||||||
This code defines a Person struct, a NewPerson function with age validation, and a Greet method.</assistant>
|
|
||||||
</turn>
|
|
||||||
|
|
||||||
<turn number="2">
|
|
||||||
<user>How can I use these in a main function?</user>
|
|
||||||
<assistant>Here's how to use the Person struct and its methods in a main function:
|
|
||||||
|
|
||||||
<step>Create a main function with error handling
|
|
||||||
```go
|
|
||||||
func main() {
|
|
||||||
// Create a new Person instance
|
|
||||||
person, err := NewPerson("Alice", 30, "alice@example.com")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error creating person:", err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the Greet method
|
fn print_year(&self) {
|
||||||
fmt.Println(person.Greet())
|
println!("Year: {}", self.year);
|
||||||
|
|
||||||
// Demonstrate error handling with invalid age
|
|
||||||
invalidPerson, err := NewPerson("Bob", 200, "bob@example.com")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error creating person with invalid age:", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
This main function demonstrates:
|
<step>
|
||||||
1. Creating a valid Person instance
|
Add a new method 'start_engine' in the Vehicle impl block
|
||||||
2. Using the Greet method
|
|
||||||
3. Handling errors when creating a Person with an invalid age</assistant>
|
|
||||||
</turn>
|
|
||||||
|
|
||||||
<turn number="3">
|
```rust
|
||||||
<user>Can you add a method to update the email with validation?</user>
|
impl Vehicle {
|
||||||
<assistant>Certainly! Let's add a method to update the email with validation:
|
// ... existing methods ...
|
||||||
|
|
||||||
<step>Add UpdateEmail method with validation
|
fn start_engine(&self) {
|
||||||
```go
|
let t0 = Instant::now();
|
||||||
import (
|
println!("Starting the engine of {} {} {}. Took: {:?}",
|
||||||
"fmt"
|
self.year, self.make, self.model, t0.elapsed());
|
||||||
"regexp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *Person) UpdateEmail(newEmail string) error {
|
|
||||||
// Simple email validation using regex
|
|
||||||
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
|
||||||
if !emailRegex.MatchString(newEmail) {
|
|
||||||
return fmt.Errorf("invalid email format: %s", newEmail)
|
|
||||||
}
|
|
||||||
p.Email = newEmail
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</step>
|
|
||||||
<step>Update the main function to demonstrate email update
|
|
||||||
```go
|
|
||||||
func main() {
|
|
||||||
// ... (previous code remains the same)
|
|
||||||
|
|
||||||
// Demonstrate email update
|
|
||||||
err = person.UpdateEmail("alice.new@example.com")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error updating email:", err)
|
|
||||||
} else {
|
|
||||||
fmt.Println("Updated greeting:", person.Greet())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Demonstrate email update with invalid email
|
|
||||||
err = person.UpdateEmail("invalid-email")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error updating email:", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
</step>
|
|
||||||
|
|
||||||
This update includes:
|
<edit>
|
||||||
1. An UpdateEmail method with email format validation
|
<path>src/vehicle.rs</path>
|
||||||
2. Demonstration of successful email update in the main function
|
<operation>append_child</operation>
|
||||||
3. Handling of invalid email update attempt</assistant>
|
<symbol>impl Vehicle</symbol>
|
||||||
</turn>
|
<description>Add start_engine method</description>
|
||||||
|
</edit>
|
||||||
|
</step>
|
||||||
|
</message>
|
||||||
|
|
||||||
</example>
|
</example>
|
||||||
</workflow>
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... other methods ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<edit>
|
||||||
|
<path>src/employee.rs</path>
|
||||||
|
<operation>delete</operation>
|
||||||
|
<symbol>struct Employee department</symbol>
|
||||||
|
</edit>
|
||||||
|
|
||||||
|
<edit>
|
||||||
|
<path>src/employee.rs</path>
|
||||||
|
<operation>update</operation>
|
||||||
|
<symbol>impl Employee fn new</symbol>
|
||||||
|
<description>Remove department parameter from new method</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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</edit>
|
||||||
|
</step>
|
||||||
|
</message>
|
||||||
|
|
||||||
|
</example>
|
||||||
|
|
||||||
|
<example language="rust">
|
||||||
|
|
||||||
|
<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,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Database {
|
||||||
|
fn new(url: &str) -> Result<Self, Error> {
|
||||||
|
let connection = Connection::connect(url)?;
|
||||||
|
Ok(Database { connection })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query(&self, sql: &str) -> Result<Vec<Row>, Error> {
|
||||||
|
self.connection.query(sql, &[])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
I want to add error handling to this module.
|
||||||
|
</message>
|
||||||
|
|
||||||
|
<message role="assistant">
|
||||||
|
<step>
|
||||||
|
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<Self, DatabaseError> {
|
||||||
|
let connection = Connection::connect(url)
|
||||||
|
.map_err(|e| DatabaseError::ConnectionError(e.to_string()))?;
|
||||||
|
Ok(Database { connection })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query(&self, sql: &str) -> Result<Vec<Row>, DatabaseError> {
|
||||||
|
self.connection.query(sql, &[])
|
||||||
|
.await
|
||||||
|
.map_err(|e| DatabaseError::QueryError(e.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<edit>
|
||||||
|
<path>src/database.rs</path>
|
||||||
|
<operation>prepend_child</operation>
|
||||||
|
<symbol>#imports</symbol>
|
||||||
|
<description>Import necessary error handling modules</description>
|
||||||
|
</edit>
|
||||||
|
|
||||||
|
<edit>
|
||||||
|
<path>src/database.rs</path>
|
||||||
|
<operation>insert_sibling_before</operation>
|
||||||
|
<symbol>pub(crate) struct Database</symbol>
|
||||||
|
<description>Define custom DatabaseError enum</description>
|
||||||
|
</edit>
|
||||||
|
|
||||||
|
<edit>
|
||||||
|
<path>src/database.rs</path>
|
||||||
|
<operation>update</operation>
|
||||||
|
<symbol>impl Database fn new</symbol>
|
||||||
|
<description>Update new method to use DatabaseError</description>
|
||||||
|
</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>
|
||||||
|
</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.
|
||||||
|
|
||||||
|
</task_description>
|
||||||
|
|
|
@ -73,6 +73,7 @@ settings.workspace = true
|
||||||
similar.workspace = true
|
similar.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
|
strum.workspace = true
|
||||||
telemetry_events.workspace = true
|
telemetry_events.workspace = true
|
||||||
terminal.workspace = true
|
terminal.workspace = true
|
||||||
terminal_view.workspace = true
|
terminal_view.workspace = true
|
||||||
|
|
|
@ -362,7 +362,7 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
|
||||||
|
|
||||||
if let Some(prompt_builder) = prompt_builder {
|
if let Some(prompt_builder) = prompt_builder {
|
||||||
slash_command_registry.register_command(
|
slash_command_registry.register_command(
|
||||||
workflow_command::WorkflowSlashCommand::new(prompt_builder),
|
workflow_command::WorkflowSlashCommand::new(prompt_builder.clone()),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -2,8 +2,8 @@
|
||||||
mod context_tests;
|
mod context_tests;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
prompts::PromptBuilder, slash_command::SlashCommandLine, workflow::WorkflowStep, MessageId,
|
prompts::PromptBuilder, slash_command::SlashCommandLine, MessageId, MessageStatus,
|
||||||
MessageStatus,
|
WorkflowStep, WorkflowStepEdit, WorkflowStepResolution, WorkflowSuggestionGroup,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
use assistant_slash_command::{
|
use assistant_slash_command::{
|
||||||
|
@ -13,10 +13,14 @@ use client::{self, proto, telemetry::Telemetry};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use fs::{Fs, RemoveOptions};
|
use fs::{Fs, RemoveOptions};
|
||||||
use futures::{future::Shared, stream::FuturesUnordered, FutureExt, StreamExt};
|
use futures::{
|
||||||
|
future::{self, Shared},
|
||||||
|
stream::FuturesUnordered,
|
||||||
|
FutureExt, StreamExt,
|
||||||
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AppContext, Context as _, EventEmitter, Image, Model, ModelContext, RenderImage, SharedString,
|
AppContext, AsyncAppContext, Context as _, EventEmitter, Image, Model, ModelContext,
|
||||||
Subscription, Task,
|
RenderImage, SharedString, Subscription, Task,
|
||||||
};
|
};
|
||||||
|
|
||||||
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
|
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
|
||||||
|
@ -30,12 +34,13 @@ use project::Project;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{
|
use std::{
|
||||||
cmp::{max, Ordering},
|
cmp::{self, max, Ordering},
|
||||||
collections::hash_map,
|
collections::hash_map,
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
iter, mem,
|
iter, mem,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
str::FromStr as _,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
@ -283,9 +288,11 @@ pub enum ContextEvent {
|
||||||
ShowAssistError(SharedString),
|
ShowAssistError(SharedString),
|
||||||
MessagesEdited,
|
MessagesEdited,
|
||||||
SummaryChanged,
|
SummaryChanged,
|
||||||
WorkflowStepsRemoved(Vec<Range<language::Anchor>>),
|
|
||||||
WorkflowStepUpdated(Range<language::Anchor>),
|
|
||||||
StreamedCompletion,
|
StreamedCompletion,
|
||||||
|
WorkflowStepsUpdated {
|
||||||
|
removed: Vec<Range<language::Anchor>>,
|
||||||
|
updated: Vec<Range<language::Anchor>>,
|
||||||
|
},
|
||||||
PendingSlashCommandsUpdated {
|
PendingSlashCommandsUpdated {
|
||||||
removed: Vec<Range<language::Anchor>>,
|
removed: Vec<Range<language::Anchor>>,
|
||||||
updated: Vec<PendingSlashCommand>,
|
updated: Vec<PendingSlashCommand>,
|
||||||
|
@ -381,8 +388,8 @@ pub struct Message {
|
||||||
pub image_offsets: SmallVec<[(usize, MessageImage); 1]>,
|
pub image_offsets: SmallVec<[(usize, MessageImage); 1]>,
|
||||||
pub offset_range: Range<usize>,
|
pub offset_range: Range<usize>,
|
||||||
pub index_range: Range<usize>,
|
pub index_range: Range<usize>,
|
||||||
|
pub anchor_range: Range<language::Anchor>,
|
||||||
pub id: MessageId,
|
pub id: MessageId,
|
||||||
pub anchor: language::Anchor,
|
|
||||||
pub role: Role,
|
pub role: Role,
|
||||||
pub status: MessageStatus,
|
pub status: MessageStatus,
|
||||||
pub cache: Option<MessageCacheMetadata>,
|
pub cache: Option<MessageCacheMetadata>,
|
||||||
|
@ -452,9 +459,23 @@ struct PendingCompletion {
|
||||||
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
||||||
pub struct SlashCommandId(clock::Lamport);
|
pub struct SlashCommandId(clock::Lamport);
|
||||||
|
|
||||||
struct WorkflowStepEntry {
|
#[derive(Clone, Debug)]
|
||||||
range: Range<language::Anchor>,
|
pub struct XmlTag {
|
||||||
step: Model<WorkflowStep>,
|
pub kind: XmlTagKind,
|
||||||
|
pub range: Range<text::Anchor>,
|
||||||
|
pub is_open_tag: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, strum::EnumString, PartialEq, Eq, strum::AsRefStr)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum XmlTagKind {
|
||||||
|
Step,
|
||||||
|
Edit,
|
||||||
|
Path,
|
||||||
|
Symbol,
|
||||||
|
Within,
|
||||||
|
Operation,
|
||||||
|
Description,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
|
@ -465,7 +486,7 @@ pub struct Context {
|
||||||
operations: Vec<ContextOperation>,
|
operations: Vec<ContextOperation>,
|
||||||
buffer: Model<Buffer>,
|
buffer: Model<Buffer>,
|
||||||
pending_slash_commands: Vec<PendingSlashCommand>,
|
pending_slash_commands: Vec<PendingSlashCommand>,
|
||||||
edits_since_last_slash_command_parse: language::Subscription,
|
edits_since_last_parse: language::Subscription,
|
||||||
finished_slash_commands: HashSet<SlashCommandId>,
|
finished_slash_commands: HashSet<SlashCommandId>,
|
||||||
slash_command_output_sections: Vec<SlashCommandOutputSection<language::Anchor>>,
|
slash_command_output_sections: Vec<SlashCommandOutputSection<language::Anchor>>,
|
||||||
message_anchors: Vec<MessageAnchor>,
|
message_anchors: Vec<MessageAnchor>,
|
||||||
|
@ -484,12 +505,34 @@ pub struct Context {
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
telemetry: Option<Arc<Telemetry>>,
|
telemetry: Option<Arc<Telemetry>>,
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
workflow_steps: Vec<WorkflowStepEntry>,
|
workflow_steps: Vec<WorkflowStep>,
|
||||||
edits_since_last_workflow_step_prune: language::Subscription,
|
xml_tags: Vec<XmlTag>,
|
||||||
project: Option<Model<Project>>,
|
project: Option<Model<Project>>,
|
||||||
prompt_builder: Arc<PromptBuilder>,
|
prompt_builder: Arc<PromptBuilder>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trait ContextAnnotation {
|
||||||
|
fn range(&self) -> &Range<language::Anchor>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextAnnotation for PendingSlashCommand {
|
||||||
|
fn range(&self) -> &Range<language::Anchor> {
|
||||||
|
&self.source_range
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextAnnotation for WorkflowStep {
|
||||||
|
fn range(&self) -> &Range<language::Anchor> {
|
||||||
|
&self.range
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextAnnotation for XmlTag {
|
||||||
|
fn range(&self) -> &Range<language::Anchor> {
|
||||||
|
&self.range
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl EventEmitter<ContextEvent> for Context {}
|
impl EventEmitter<ContextEvent> for Context {}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
|
@ -535,8 +578,6 @@ impl Context {
|
||||||
});
|
});
|
||||||
let edits_since_last_slash_command_parse =
|
let edits_since_last_slash_command_parse =
|
||||||
buffer.update(cx, |buffer, _| buffer.subscribe());
|
buffer.update(cx, |buffer, _| buffer.subscribe());
|
||||||
let edits_since_last_workflow_step_prune =
|
|
||||||
buffer.update(cx, |buffer, _| buffer.subscribe());
|
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
id,
|
id,
|
||||||
timestamp: clock::Lamport::new(replica_id),
|
timestamp: clock::Lamport::new(replica_id),
|
||||||
|
@ -550,7 +591,7 @@ impl Context {
|
||||||
pending_slash_commands: Vec::new(),
|
pending_slash_commands: Vec::new(),
|
||||||
finished_slash_commands: HashSet::default(),
|
finished_slash_commands: HashSet::default(),
|
||||||
slash_command_output_sections: Vec::new(),
|
slash_command_output_sections: Vec::new(),
|
||||||
edits_since_last_slash_command_parse,
|
edits_since_last_parse: edits_since_last_slash_command_parse,
|
||||||
summary: None,
|
summary: None,
|
||||||
pending_summary: Task::ready(None),
|
pending_summary: Task::ready(None),
|
||||||
completion_count: Default::default(),
|
completion_count: Default::default(),
|
||||||
|
@ -566,7 +607,7 @@ impl Context {
|
||||||
project,
|
project,
|
||||||
language_registry,
|
language_registry,
|
||||||
workflow_steps: Vec::new(),
|
workflow_steps: Vec::new(),
|
||||||
edits_since_last_workflow_step_prune,
|
xml_tags: Vec::new(),
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -753,7 +794,7 @@ impl Context {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn flush_ops(&mut self, cx: &mut ModelContext<Context>) {
|
fn flush_ops(&mut self, cx: &mut ModelContext<Context>) {
|
||||||
let mut messages_changed = false;
|
let mut changed_messages = HashSet::default();
|
||||||
let mut summary_changed = false;
|
let mut summary_changed = false;
|
||||||
|
|
||||||
self.pending_ops.sort_unstable_by_key(|op| op.timestamp());
|
self.pending_ops.sort_unstable_by_key(|op| op.timestamp());
|
||||||
|
@ -771,8 +812,8 @@ impl Context {
|
||||||
if self.messages_metadata.contains_key(&anchor.id) {
|
if self.messages_metadata.contains_key(&anchor.id) {
|
||||||
// We already applied this operation.
|
// We already applied this operation.
|
||||||
} else {
|
} else {
|
||||||
|
changed_messages.insert(anchor.id);
|
||||||
self.insert_message(anchor, metadata, cx);
|
self.insert_message(anchor, metadata, cx);
|
||||||
messages_changed = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContextOperation::UpdateMessage {
|
ContextOperation::UpdateMessage {
|
||||||
|
@ -783,7 +824,7 @@ impl Context {
|
||||||
let metadata = self.messages_metadata.get_mut(&message_id).unwrap();
|
let metadata = self.messages_metadata.get_mut(&message_id).unwrap();
|
||||||
if new_metadata.timestamp > metadata.timestamp {
|
if new_metadata.timestamp > metadata.timestamp {
|
||||||
*metadata = new_metadata;
|
*metadata = new_metadata;
|
||||||
messages_changed = true;
|
changed_messages.insert(message_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContextOperation::UpdateSummary {
|
ContextOperation::UpdateSummary {
|
||||||
|
@ -827,7 +868,8 @@ impl Context {
|
||||||
self.operations.push(op);
|
self.operations.push(op);
|
||||||
}
|
}
|
||||||
|
|
||||||
if messages_changed {
|
if !changed_messages.is_empty() {
|
||||||
|
self.message_roles_updated(changed_messages, cx);
|
||||||
cx.emit(ContextEvent::MessagesEdited);
|
cx.emit(ContextEvent::MessagesEdited);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
@ -908,11 +950,11 @@ impl Context {
|
||||||
self.summary.as_ref()
|
self.summary.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn workflow_step_containing(
|
pub(crate) fn workflow_step_containing(
|
||||||
&self,
|
&self,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> Option<(Range<language::Anchor>, Model<WorkflowStep>)> {
|
) -> Option<&WorkflowStep> {
|
||||||
let buffer = self.buffer.read(cx);
|
let buffer = self.buffer.read(cx);
|
||||||
let index = self
|
let index = self
|
||||||
.workflow_steps
|
.workflow_steps
|
||||||
|
@ -927,21 +969,24 @@ impl Context {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.ok()?;
|
.ok()?;
|
||||||
let step = &self.workflow_steps[index];
|
Some(&self.workflow_steps[index])
|
||||||
Some((step.range.clone(), step.step.clone()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn workflow_step_for_range(
|
pub fn workflow_step_ranges(&self) -> impl Iterator<Item = Range<language::Anchor>> + '_ {
|
||||||
|
self.workflow_steps.iter().map(|step| step.range.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn workflow_step_for_range(
|
||||||
&self,
|
&self,
|
||||||
range: Range<language::Anchor>,
|
range: &Range<language::Anchor>,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> Option<Model<WorkflowStep>> {
|
) -> Option<&WorkflowStep> {
|
||||||
let buffer = self.buffer.read(cx);
|
let buffer = self.buffer.read(cx);
|
||||||
let index = self.workflow_step_index_for_range(&range, buffer).ok()?;
|
let index = self.workflow_step_index_for_range(range, buffer).ok()?;
|
||||||
Some(self.workflow_steps[index].step.clone())
|
Some(&self.workflow_steps[index])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn workflow_step_index_for_range(
|
fn workflow_step_index_for_range(
|
||||||
&self,
|
&self,
|
||||||
tagged_range: &Range<text::Anchor>,
|
tagged_range: &Range<text::Anchor>,
|
||||||
buffer: &text::BufferSnapshot,
|
buffer: &text::BufferSnapshot,
|
||||||
|
@ -982,10 +1027,9 @@ impl Context {
|
||||||
)),
|
)),
|
||||||
language::Event::Edited => {
|
language::Event::Edited => {
|
||||||
self.count_remaining_tokens(cx);
|
self.count_remaining_tokens(cx);
|
||||||
self.reparse_slash_commands(cx);
|
self.reparse(cx);
|
||||||
// Use `inclusive = true` to invalidate a step when an edit occurs
|
// Use `inclusive = true` to invalidate a step when an edit occurs
|
||||||
// at the start/end of a parsed step.
|
// at the start/end of a parsed step.
|
||||||
self.prune_invalid_workflow_steps(true, cx);
|
|
||||||
cx.emit(ContextEvent::MessagesEdited);
|
cx.emit(ContextEvent::MessagesEdited);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
@ -1199,10 +1243,10 @@ impl Context {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reparse_slash_commands(&mut self, cx: &mut ModelContext<Self>) {
|
pub fn reparse(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
let buffer = self.buffer.read(cx);
|
let buffer = self.buffer.read(cx).text_snapshot();
|
||||||
let mut row_ranges = self
|
let mut row_ranges = self
|
||||||
.edits_since_last_slash_command_parse
|
.edits_since_last_parse
|
||||||
.consume()
|
.consume()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|edit| {
|
.map(|edit| {
|
||||||
|
@ -1212,8 +1256,10 @@ impl Context {
|
||||||
})
|
})
|
||||||
.peekable();
|
.peekable();
|
||||||
|
|
||||||
let mut removed = Vec::new();
|
let mut removed_slash_command_ranges = Vec::new();
|
||||||
let mut updated = Vec::new();
|
let mut updated_slash_commands = Vec::new();
|
||||||
|
let mut removed_steps = Vec::new();
|
||||||
|
let mut updated_steps = Vec::new();
|
||||||
while let Some(mut row_range) = row_ranges.next() {
|
while let Some(mut row_range) = row_ranges.next() {
|
||||||
while let Some(next_row_range) = row_ranges.peek() {
|
while let Some(next_row_range) = row_ranges.peek() {
|
||||||
if row_range.end >= next_row_range.start {
|
if row_range.end >= next_row_range.start {
|
||||||
|
@ -1230,10 +1276,49 @@ impl Context {
|
||||||
buffer.line_len(row_range.end - 1),
|
buffer.line_len(row_range.end - 1),
|
||||||
));
|
));
|
||||||
|
|
||||||
let old_range = self.pending_command_indices_for_range(start..end, cx);
|
self.reparse_slash_commands_in_range(
|
||||||
|
start..end,
|
||||||
|
&buffer,
|
||||||
|
&mut updated_slash_commands,
|
||||||
|
&mut removed_slash_command_ranges,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
self.reparse_workflow_steps_in_range(
|
||||||
|
start..end,
|
||||||
|
&buffer,
|
||||||
|
&mut updated_steps,
|
||||||
|
&mut removed_steps,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !updated_slash_commands.is_empty() || !removed_slash_command_ranges.is_empty() {
|
||||||
|
cx.emit(ContextEvent::PendingSlashCommandsUpdated {
|
||||||
|
removed: removed_slash_command_ranges,
|
||||||
|
updated: updated_slash_commands,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if !updated_steps.is_empty() || !removed_steps.is_empty() {
|
||||||
|
cx.emit(ContextEvent::WorkflowStepsUpdated {
|
||||||
|
removed: removed_steps,
|
||||||
|
updated: updated_steps,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reparse_slash_commands_in_range(
|
||||||
|
&mut self,
|
||||||
|
range: Range<text::Anchor>,
|
||||||
|
buffer: &BufferSnapshot,
|
||||||
|
updated: &mut Vec<PendingSlashCommand>,
|
||||||
|
removed: &mut Vec<Range<text::Anchor>>,
|
||||||
|
cx: &AppContext,
|
||||||
|
) {
|
||||||
|
let old_range = self.pending_command_indices_for_range(range.clone(), cx);
|
||||||
|
|
||||||
let mut new_commands = Vec::new();
|
let mut new_commands = Vec::new();
|
||||||
let mut lines = buffer.text_for_range(start..end).lines();
|
let mut lines = buffer.text_for_range(range).lines();
|
||||||
let mut offset = lines.offset();
|
let mut offset = lines.offset();
|
||||||
while let Some(line) = lines.next() {
|
while let Some(line) = lines.next() {
|
||||||
if let Some(command_line) = SlashCommandLine::parse(line) {
|
if let Some(command_line) = SlashCommandLine::parse(line) {
|
||||||
|
@ -1279,168 +1364,369 @@ impl Context {
|
||||||
removed.extend(removed_commands.map(|command| command.source_range));
|
removed.extend(removed_commands.map(|command| command.source_range));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !updated.is_empty() || !removed.is_empty() {
|
fn reparse_workflow_steps_in_range(
|
||||||
cx.emit(ContextEvent::PendingSlashCommandsUpdated { removed, updated });
|
&mut self,
|
||||||
|
range: Range<text::Anchor>,
|
||||||
|
buffer: &BufferSnapshot,
|
||||||
|
updated: &mut Vec<Range<text::Anchor>>,
|
||||||
|
removed: &mut Vec<Range<text::Anchor>>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
|
// Rebuild the XML tags in the edited range.
|
||||||
|
let intersecting_tags_range =
|
||||||
|
self.indices_intersecting_buffer_range(&self.xml_tags, range.clone(), cx);
|
||||||
|
let new_tags = self.parse_xml_tags_in_range(buffer, range.clone(), cx);
|
||||||
|
self.xml_tags
|
||||||
|
.splice(intersecting_tags_range.clone(), new_tags);
|
||||||
|
|
||||||
|
// Find which steps intersect the changed range.
|
||||||
|
let intersecting_steps_range =
|
||||||
|
self.indices_intersecting_buffer_range(&self.workflow_steps, range.clone(), cx);
|
||||||
|
|
||||||
|
// Reparse all tags after the last unchanged step before the change.
|
||||||
|
let mut tags_start_ix = 0;
|
||||||
|
if let Some(preceding_unchanged_step) =
|
||||||
|
self.workflow_steps[..intersecting_steps_range.start].last()
|
||||||
|
{
|
||||||
|
tags_start_ix = match self.xml_tags.binary_search_by(|tag| {
|
||||||
|
tag.range
|
||||||
|
.start
|
||||||
|
.cmp(&preceding_unchanged_step.range.end, buffer)
|
||||||
|
.then(Ordering::Less)
|
||||||
|
}) {
|
||||||
|
Ok(ix) | Err(ix) => ix,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild the edit suggestions in the range.
|
||||||
|
let mut new_steps = self.parse_steps(tags_start_ix, range.end, buffer);
|
||||||
|
|
||||||
|
if let Some(project) = self.project() {
|
||||||
|
for step in &mut new_steps {
|
||||||
|
Self::resolve_workflow_step_internal(step, &project, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prune_invalid_workflow_steps(&mut self, inclusive: bool, cx: &mut ModelContext<Self>) {
|
updated.extend(new_steps.iter().map(|step| step.range.clone()));
|
||||||
let mut removed = Vec::new();
|
let removed_steps = self
|
||||||
|
.workflow_steps
|
||||||
for edit_range in self.edits_since_last_workflow_step_prune.consume() {
|
.splice(intersecting_steps_range, new_steps);
|
||||||
let intersecting_range = self.find_intersecting_steps(edit_range.new, inclusive, cx);
|
|
||||||
removed.extend(
|
removed.extend(
|
||||||
self.workflow_steps
|
removed_steps
|
||||||
.drain(intersecting_range)
|
.map(|step| step.range)
|
||||||
.map(|step| step.range),
|
.filter(|range| !updated.contains(&range)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !removed.is_empty() {
|
fn parse_xml_tags_in_range(
|
||||||
cx.emit(ContextEvent::WorkflowStepsRemoved(removed));
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_intersecting_steps(
|
|
||||||
&self,
|
&self,
|
||||||
range: Range<usize>,
|
buffer: &BufferSnapshot,
|
||||||
inclusive: bool,
|
range: Range<text::Anchor>,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> Range<usize> {
|
) -> Vec<XmlTag> {
|
||||||
let buffer = self.buffer.read(cx);
|
let mut messages = self.messages(cx).peekable();
|
||||||
let start_ix = match self.workflow_steps.binary_search_by(|probe| {
|
|
||||||
probe
|
let mut tags = Vec::new();
|
||||||
.range
|
let mut lines = buffer.text_for_range(range).lines();
|
||||||
.end
|
let mut offset = lines.offset();
|
||||||
.to_offset(buffer)
|
|
||||||
.cmp(&range.start)
|
while let Some(line) = lines.next() {
|
||||||
.then(if inclusive {
|
while let Some(message) = messages.peek() {
|
||||||
Ordering::Greater
|
if offset < message.offset_range.end {
|
||||||
|
break;
|
||||||
} else {
|
} else {
|
||||||
Ordering::Less
|
messages.next();
|
||||||
})
|
}
|
||||||
}) {
|
|
||||||
Ok(ix) | Err(ix) => ix,
|
|
||||||
};
|
|
||||||
let end_ix = match self.workflow_steps.binary_search_by(|probe| {
|
|
||||||
probe
|
|
||||||
.range
|
|
||||||
.start
|
|
||||||
.to_offset(buffer)
|
|
||||||
.cmp(&range.end)
|
|
||||||
.then(if inclusive {
|
|
||||||
Ordering::Less
|
|
||||||
} else {
|
|
||||||
Ordering::Greater
|
|
||||||
})
|
|
||||||
}) {
|
|
||||||
Ok(ix) | Err(ix) => ix,
|
|
||||||
};
|
|
||||||
start_ix..end_ix
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_workflow_steps_in_range(&mut self, range: Range<usize>, cx: &mut ModelContext<Self>) {
|
let is_assistant_message = messages
|
||||||
let weak_self = cx.weak_model();
|
.peek()
|
||||||
let mut new_edit_steps = Vec::new();
|
.map_or(false, |message| message.role == Role::Assistant);
|
||||||
|
if is_assistant_message {
|
||||||
|
for (start_ix, _) in line.match_indices('<') {
|
||||||
|
let mut name_start_ix = start_ix + 1;
|
||||||
|
let closing_bracket_ix = line[start_ix..].find('>').map(|i| start_ix + i);
|
||||||
|
if let Some(closing_bracket_ix) = closing_bracket_ix {
|
||||||
|
let end_ix = closing_bracket_ix + 1;
|
||||||
|
let mut is_open_tag = true;
|
||||||
|
if line[name_start_ix..closing_bracket_ix].starts_with('/') {
|
||||||
|
name_start_ix += 1;
|
||||||
|
is_open_tag = false;
|
||||||
|
}
|
||||||
|
let tag_inner = &line[name_start_ix..closing_bracket_ix];
|
||||||
|
let tag_name_len = tag_inner
|
||||||
|
.find(|c: char| c.is_whitespace())
|
||||||
|
.unwrap_or(tag_inner.len());
|
||||||
|
if let Ok(kind) = XmlTagKind::from_str(&tag_inner[..tag_name_len]) {
|
||||||
|
tags.push(XmlTag {
|
||||||
|
range: buffer.anchor_after(offset + start_ix)
|
||||||
|
..buffer.anchor_before(offset + end_ix),
|
||||||
|
is_open_tag,
|
||||||
|
kind,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
offset = lines.offset();
|
||||||
|
}
|
||||||
|
tags
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_steps(
|
||||||
|
&mut self,
|
||||||
|
tags_start_ix: usize,
|
||||||
|
buffer_end: text::Anchor,
|
||||||
|
buffer: &BufferSnapshot,
|
||||||
|
) -> Vec<WorkflowStep> {
|
||||||
|
let mut new_steps = Vec::new();
|
||||||
|
let mut pending_step = None;
|
||||||
|
let mut edit_step_depth = 0;
|
||||||
|
let mut tags = self.xml_tags[tags_start_ix..].iter().peekable();
|
||||||
|
'tags: while let Some(tag) = tags.next() {
|
||||||
|
if tag.range.start.cmp(&buffer_end, buffer).is_gt() && edit_step_depth == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if tag.kind == XmlTagKind::Step && tag.is_open_tag {
|
||||||
|
edit_step_depth += 1;
|
||||||
|
let edit_start = tag.range.start;
|
||||||
let mut edits = Vec::new();
|
let mut edits = Vec::new();
|
||||||
|
let mut step = WorkflowStep {
|
||||||
|
range: edit_start..edit_start,
|
||||||
|
leading_tags_end: tag.range.end,
|
||||||
|
trailing_tag_start: None,
|
||||||
|
edits: Default::default(),
|
||||||
|
resolution: None,
|
||||||
|
resolution_task: None,
|
||||||
|
};
|
||||||
|
|
||||||
let buffer = self.buffer.read(cx).snapshot();
|
while let Some(tag) = tags.next() {
|
||||||
let mut message_lines = buffer.as_rope().chunks_in_range(range).lines();
|
step.trailing_tag_start.get_or_insert(tag.range.start);
|
||||||
let mut in_step = false;
|
|
||||||
let mut step_open_tag_start_ix = 0;
|
|
||||||
let mut line_start_offset = message_lines.offset();
|
|
||||||
|
|
||||||
while let Some(line) = message_lines.next() {
|
if tag.kind == XmlTagKind::Step && !tag.is_open_tag {
|
||||||
if let Some(step_start_index) = line.find("<step>") {
|
// step.trailing_tag_start = Some(tag.range.start);
|
||||||
if !in_step {
|
edit_step_depth -= 1;
|
||||||
in_step = true;
|
if edit_step_depth == 0 {
|
||||||
step_open_tag_start_ix = line_start_offset + step_start_index;
|
step.range.end = tag.range.end;
|
||||||
|
step.edits = edits.into();
|
||||||
|
new_steps.push(step);
|
||||||
|
continue 'tags;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(step_end_index) = line.find("</step>") {
|
if tag.kind == XmlTagKind::Edit && tag.is_open_tag {
|
||||||
if in_step {
|
let mut path = None;
|
||||||
let mut step_open_tag_end_ix = step_open_tag_start_ix + "<step>".len();
|
let mut symbol = None;
|
||||||
if buffer.chars_at(step_open_tag_end_ix).next() == Some('\n') {
|
let mut operation = None;
|
||||||
step_open_tag_end_ix += 1;
|
let mut description = None;
|
||||||
}
|
|
||||||
let mut step_end_tag_start_ix = line_start_offset + step_end_index;
|
|
||||||
let step_end_tag_end_ix = step_end_tag_start_ix + "</step>".len();
|
|
||||||
if buffer.reversed_chars_at(step_end_tag_start_ix).next() == Some('\n') {
|
|
||||||
step_end_tag_start_ix -= 1;
|
|
||||||
}
|
|
||||||
edits.push((step_open_tag_start_ix..step_open_tag_end_ix, ""));
|
|
||||||
edits.push((step_end_tag_start_ix..step_end_tag_end_ix, ""));
|
|
||||||
let tagged_range = buffer.anchor_after(step_open_tag_end_ix)
|
|
||||||
..buffer.anchor_before(step_end_tag_start_ix);
|
|
||||||
|
|
||||||
// Check if a step with the same range already exists
|
while let Some(tag) = tags.next() {
|
||||||
let existing_step_index =
|
if tag.kind == XmlTagKind::Edit && !tag.is_open_tag {
|
||||||
self.workflow_step_index_for_range(&tagged_range, &buffer);
|
edits.push(WorkflowStepEdit::new(
|
||||||
|
path,
|
||||||
if let Err(ix) = existing_step_index {
|
operation,
|
||||||
new_edit_steps.push((
|
symbol,
|
||||||
ix,
|
description,
|
||||||
WorkflowStepEntry {
|
|
||||||
step: cx.new_model(|_| {
|
|
||||||
WorkflowStep::new(tagged_range.clone(), weak_self.clone())
|
|
||||||
}),
|
|
||||||
range: tagged_range,
|
|
||||||
},
|
|
||||||
));
|
));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
in_step = false;
|
if tag.is_open_tag
|
||||||
|
&& [
|
||||||
|
XmlTagKind::Path,
|
||||||
|
XmlTagKind::Symbol,
|
||||||
|
XmlTagKind::Operation,
|
||||||
|
XmlTagKind::Description,
|
||||||
|
]
|
||||||
|
.contains(&tag.kind)
|
||||||
|
{
|
||||||
|
let kind = tag.kind;
|
||||||
|
let content_start = tag.range.end;
|
||||||
|
if let Some(tag) = tags.peek() {
|
||||||
|
if tag.kind == kind && !tag.is_open_tag {
|
||||||
|
let tag = tags.next().unwrap();
|
||||||
|
let content_end = tag.range.start;
|
||||||
|
let mut content = buffer
|
||||||
|
.text_for_range(content_start..content_end)
|
||||||
|
.collect::<String>();
|
||||||
|
content.truncate(content.trim_end().len());
|
||||||
|
match kind {
|
||||||
|
XmlTagKind::Path => path = Some(content),
|
||||||
|
XmlTagKind::Operation => operation = Some(content),
|
||||||
|
XmlTagKind::Symbol => {
|
||||||
|
symbol = Some(content).filter(|s| !s.is_empty())
|
||||||
|
}
|
||||||
|
XmlTagKind::Description => {
|
||||||
|
description =
|
||||||
|
Some(content).filter(|s| !s.is_empty())
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
line_start_offset = message_lines.offset();
|
pending_step = Some(step);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut updated = Vec::new();
|
if let Some(mut pending_step) = pending_step {
|
||||||
for (index, step) in new_edit_steps.into_iter().rev() {
|
pending_step.range.end = text::Anchor::MAX;
|
||||||
let step_range = step.range.clone();
|
new_steps.push(pending_step);
|
||||||
updated.push(step_range.clone());
|
|
||||||
self.workflow_steps.insert(index, step);
|
|
||||||
self.resolve_workflow_step(step_range, cx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete <step> tags, making sure we don't accidentally invalidate
|
new_steps
|
||||||
// the step we just parsed.
|
|
||||||
self.buffer
|
|
||||||
.update(cx, |buffer, cx| buffer.edit(edits, None, cx));
|
|
||||||
self.edits_since_last_workflow_step_prune.consume();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_workflow_step(
|
pub fn resolve_workflow_step(
|
||||||
&mut self,
|
&mut self,
|
||||||
tagged_range: Range<language::Anchor>,
|
tagged_range: Range<text::Anchor>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) -> Option<()> {
|
||||||
let Ok(step_index) = self
|
let index = self
|
||||||
.workflow_steps
|
.workflow_step_index_for_range(&tagged_range, self.buffer.read(cx))
|
||||||
.binary_search_by(|step| step.range.cmp(&tagged_range, self.buffer.read(cx)))
|
.ok()?;
|
||||||
else {
|
let step = &mut self.workflow_steps[index];
|
||||||
return;
|
let project = self.project.as_ref()?;
|
||||||
};
|
step.resolution.take();
|
||||||
|
Self::resolve_workflow_step_internal(step, project, cx);
|
||||||
cx.emit(ContextEvent::WorkflowStepUpdated(tagged_range.clone()));
|
None
|
||||||
cx.notify();
|
|
||||||
|
|
||||||
let resolution = self.workflow_steps[step_index].step.clone();
|
|
||||||
cx.defer(move |cx| {
|
|
||||||
resolution.update(cx, |resolution, cx| resolution.resolve(cx));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn workflow_step_updated(
|
fn resolve_workflow_step_internal(
|
||||||
&mut self,
|
step: &mut WorkflowStep,
|
||||||
range: Range<language::Anchor>,
|
project: &Model<Project>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<'_, Context>,
|
||||||
) {
|
) {
|
||||||
cx.emit(ContextEvent::WorkflowStepUpdated(range));
|
step.resolution_task = Some(cx.spawn({
|
||||||
cx.notify();
|
let range = step.range.clone();
|
||||||
|
let edits = step.edits.clone();
|
||||||
|
let project = project.clone();
|
||||||
|
|this, mut cx| async move {
|
||||||
|
let suggestion_groups =
|
||||||
|
Self::compute_step_resolution(project, edits, &mut cx).await;
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
let buffer = this.buffer.read(cx).text_snapshot();
|
||||||
|
let ix = this.workflow_step_index_for_range(&range, &buffer).ok();
|
||||||
|
if let Some(ix) = ix {
|
||||||
|
let step = &mut this.workflow_steps[ix];
|
||||||
|
|
||||||
|
let resolution = suggestion_groups.map(|suggestion_groups| {
|
||||||
|
let mut title = String::new();
|
||||||
|
for mut chunk in buffer.text_for_range(
|
||||||
|
step.leading_tags_end
|
||||||
|
..step.trailing_tag_start.unwrap_or(step.range.end),
|
||||||
|
) {
|
||||||
|
if title.is_empty() {
|
||||||
|
chunk = chunk.trim_start();
|
||||||
|
}
|
||||||
|
if let Some((prefix, _)) = chunk.split_once('\n') {
|
||||||
|
title.push_str(prefix);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
title.push_str(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkflowStepResolution {
|
||||||
|
title,
|
||||||
|
suggestion_groups,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
step.resolution = Some(Arc::new(resolution));
|
||||||
|
cx.emit(ContextEvent::WorkflowStepsUpdated {
|
||||||
|
removed: vec![],
|
||||||
|
updated: vec![range],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn compute_step_resolution(
|
||||||
|
project: Model<Project>,
|
||||||
|
edits: Arc<[Result<WorkflowStepEdit>]>,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> Result<HashMap<Model<Buffer>, Vec<WorkflowSuggestionGroup>>> {
|
||||||
|
let mut suggestion_tasks = Vec::new();
|
||||||
|
for edit in edits.iter() {
|
||||||
|
let edit = edit.as_ref().map_err(|e| anyhow!("{e}"))?;
|
||||||
|
suggestion_tasks.push(edit.resolve(project.clone(), cx.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand the context ranges of each suggestion and group suggestions with overlapping context ranges.
|
||||||
|
let suggestions = future::try_join_all(suggestion_tasks).await?;
|
||||||
|
|
||||||
|
let mut suggestions_by_buffer = HashMap::default();
|
||||||
|
for (buffer, suggestion) in suggestions {
|
||||||
|
suggestions_by_buffer
|
||||||
|
.entry(buffer)
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(suggestion);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut suggestion_groups_by_buffer = HashMap::default();
|
||||||
|
for (buffer, mut suggestions) in suggestions_by_buffer {
|
||||||
|
let mut suggestion_groups = Vec::<WorkflowSuggestionGroup>::new();
|
||||||
|
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
|
||||||
|
// Sort suggestions by their range so that earlier, larger ranges come first
|
||||||
|
suggestions.sort_by(|a, b| a.range().cmp(&b.range(), &snapshot));
|
||||||
|
|
||||||
|
// Merge overlapping suggestions
|
||||||
|
suggestions.dedup_by(|a, b| b.try_merge(a, &snapshot));
|
||||||
|
|
||||||
|
// Create context ranges for each suggestion
|
||||||
|
for suggestion in suggestions {
|
||||||
|
let context_range = {
|
||||||
|
let suggestion_point_range = suggestion.range().to_point(&snapshot);
|
||||||
|
let start_row = suggestion_point_range.start.row.saturating_sub(5);
|
||||||
|
let end_row =
|
||||||
|
cmp::min(suggestion_point_range.end.row + 5, snapshot.max_point().row);
|
||||||
|
let start = snapshot.anchor_before(Point::new(start_row, 0));
|
||||||
|
let end =
|
||||||
|
snapshot.anchor_after(Point::new(end_row, snapshot.line_len(end_row)));
|
||||||
|
start..end
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(last_group) = suggestion_groups.last_mut() {
|
||||||
|
if last_group
|
||||||
|
.context_range
|
||||||
|
.end
|
||||||
|
.cmp(&context_range.start, &snapshot)
|
||||||
|
.is_ge()
|
||||||
|
{
|
||||||
|
// Merge with the previous group if context ranges overlap
|
||||||
|
last_group.context_range.end = context_range.end;
|
||||||
|
last_group.suggestions.push(suggestion);
|
||||||
|
} else {
|
||||||
|
// Create a new group
|
||||||
|
suggestion_groups.push(WorkflowSuggestionGroup {
|
||||||
|
context_range,
|
||||||
|
suggestions: vec![suggestion],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create the first group
|
||||||
|
suggestion_groups.push(WorkflowSuggestionGroup {
|
||||||
|
context_range,
|
||||||
|
suggestions: vec![suggestion],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suggestion_groups_by_buffer.insert(buffer, suggestion_groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(suggestion_groups_by_buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pending_command_for_position(
|
pub fn pending_command_for_position(
|
||||||
|
@ -1480,17 +1766,24 @@ impl Context {
|
||||||
&self,
|
&self,
|
||||||
range: Range<language::Anchor>,
|
range: Range<language::Anchor>,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
|
) -> Range<usize> {
|
||||||
|
self.indices_intersecting_buffer_range(&self.pending_slash_commands, range, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn indices_intersecting_buffer_range<T: ContextAnnotation>(
|
||||||
|
&self,
|
||||||
|
all_annotations: &[T],
|
||||||
|
range: Range<language::Anchor>,
|
||||||
|
cx: &AppContext,
|
||||||
) -> Range<usize> {
|
) -> Range<usize> {
|
||||||
let buffer = self.buffer.read(cx);
|
let buffer = self.buffer.read(cx);
|
||||||
let start_ix = match self
|
let start_ix = match all_annotations
|
||||||
.pending_slash_commands
|
.binary_search_by(|probe| probe.range().end.cmp(&range.start, &buffer))
|
||||||
.binary_search_by(|probe| probe.source_range.end.cmp(&range.start, &buffer))
|
|
||||||
{
|
{
|
||||||
Ok(ix) | Err(ix) => ix,
|
Ok(ix) | Err(ix) => ix,
|
||||||
};
|
};
|
||||||
let end_ix = match self
|
let end_ix = match all_annotations
|
||||||
.pending_slash_commands
|
.binary_search_by(|probe| probe.range().start.cmp(&range.end, &buffer))
|
||||||
.binary_search_by(|probe| probe.source_range.start.cmp(&range.end, &buffer))
|
|
||||||
{
|
{
|
||||||
Ok(ix) => ix + 1,
|
Ok(ix) => ix + 1,
|
||||||
Err(ix) => ix,
|
Err(ix) => ix,
|
||||||
|
@ -1506,7 +1799,7 @@ impl Context {
|
||||||
expand_result: bool,
|
expand_result: bool,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
self.reparse_slash_commands(cx);
|
self.reparse(cx);
|
||||||
|
|
||||||
let insert_output_task = cx.spawn(|this, mut cx| {
|
let insert_output_task = cx.spawn(|this, mut cx| {
|
||||||
let command_range = command_range.clone();
|
let command_range = command_range.clone();
|
||||||
|
@ -1670,27 +1963,21 @@ impl Context {
|
||||||
.message_anchors
|
.message_anchors
|
||||||
.iter()
|
.iter()
|
||||||
.position(|message| message.id == assistant_message_id)?;
|
.position(|message| message.id == assistant_message_id)?;
|
||||||
let message_range = this.buffer.update(cx, |buffer, cx| {
|
this.buffer.update(cx, |buffer, cx| {
|
||||||
let message_start_offset =
|
|
||||||
this.message_anchors[message_ix].start.to_offset(buffer);
|
|
||||||
let message_old_end_offset = this.message_anchors[message_ix + 1..]
|
let message_old_end_offset = this.message_anchors[message_ix + 1..]
|
||||||
.iter()
|
.iter()
|
||||||
.find(|message| message.start.is_valid(buffer))
|
.find(|message| message.start.is_valid(buffer))
|
||||||
.map_or(buffer.len(), |message| {
|
.map_or(buffer.len(), |message| {
|
||||||
message.start.to_offset(buffer).saturating_sub(1)
|
message.start.to_offset(buffer).saturating_sub(1)
|
||||||
});
|
});
|
||||||
let message_new_end_offset = message_old_end_offset + chunk.len();
|
|
||||||
buffer.edit(
|
buffer.edit(
|
||||||
[(message_old_end_offset..message_old_end_offset, chunk)],
|
[(message_old_end_offset..message_old_end_offset, chunk)],
|
||||||
None,
|
None,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
message_start_offset..message_new_end_offset
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use `inclusive = false` as edits might occur at the end of a parsed step.
|
// Use `inclusive = false` as edits might occur at the end of a parsed step.
|
||||||
this.prune_invalid_workflow_steps(false, cx);
|
|
||||||
this.parse_workflow_steps_in_range(message_range, cx);
|
|
||||||
cx.emit(ContextEvent::StreamedCompletion);
|
cx.emit(ContextEvent::StreamedCompletion);
|
||||||
|
|
||||||
Some(())
|
Some(())
|
||||||
|
@ -1781,12 +2068,34 @@ impl Context {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cycle_message_roles(&mut self, ids: HashSet<MessageId>, cx: &mut ModelContext<Self>) {
|
pub fn cycle_message_roles(&mut self, ids: HashSet<MessageId>, cx: &mut ModelContext<Self>) {
|
||||||
for id in ids {
|
for id in &ids {
|
||||||
if let Some(metadata) = self.messages_metadata.get(&id) {
|
if let Some(metadata) = self.messages_metadata.get(id) {
|
||||||
let role = metadata.role.cycle();
|
let role = metadata.role.cycle();
|
||||||
self.update_metadata(id, cx, |metadata| metadata.role = role);
|
self.update_metadata(*id, cx, |metadata| metadata.role = role);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.message_roles_updated(ids, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn message_roles_updated(&mut self, ids: HashSet<MessageId>, cx: &mut ModelContext<Self>) {
|
||||||
|
let mut ranges = Vec::new();
|
||||||
|
for message in self.messages(cx) {
|
||||||
|
if ids.contains(&message.id) {
|
||||||
|
ranges.push(message.anchor_range.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let buffer = self.buffer.read(cx).text_snapshot();
|
||||||
|
let mut updated = Vec::new();
|
||||||
|
let mut removed = Vec::new();
|
||||||
|
for range in ranges {
|
||||||
|
self.reparse_workflow_steps_in_range(range, &buffer, &mut updated, &mut removed, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !updated.is_empty() || !removed.is_empty() {
|
||||||
|
cx.emit(ContextEvent::WorkflowStepsUpdated { removed, updated })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_metadata(
|
pub fn update_metadata(
|
||||||
|
@ -2249,8 +2558,8 @@ impl Context {
|
||||||
return Some(Message {
|
return Some(Message {
|
||||||
index_range: start_ix..end_ix,
|
index_range: start_ix..end_ix,
|
||||||
offset_range: message_start..message_end,
|
offset_range: message_start..message_end,
|
||||||
|
anchor_range: message_anchor.start..message_end_anchor,
|
||||||
id: message_anchor.id,
|
id: message_anchor.id,
|
||||||
anchor: message_anchor.start,
|
|
||||||
role: metadata.role,
|
role: metadata.role,
|
||||||
status: metadata.status.clone(),
|
status: metadata.status.clone(),
|
||||||
cache: metadata.cache.clone(),
|
cache: metadata.cache.clone(),
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
use super::{MessageCacheMetadata, WorkflowStepEdit};
|
||||||
use crate::{
|
use crate::{
|
||||||
assistant_panel, prompt_library, slash_command::file_command, workflow::tool, CacheStatus,
|
assistant_panel, prompt_library, slash_command::file_command, CacheStatus, Context,
|
||||||
Context, ContextEvent, ContextId, ContextOperation, MessageId, MessageStatus, PromptBuilder,
|
ContextEvent, ContextId, ContextOperation, MessageId, MessageStatus, PromptBuilder,
|
||||||
|
WorkflowStepEditKind,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use assistant_slash_command::{
|
use assistant_slash_command::{
|
||||||
|
@ -8,15 +10,13 @@ use assistant_slash_command::{
|
||||||
SlashCommandRegistry,
|
SlashCommandRegistry,
|
||||||
};
|
};
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use fs::{FakeFs, Fs as _};
|
use fs::FakeFs;
|
||||||
use gpui::{AppContext, Model, SharedString, Task, TestAppContext, WeakView};
|
use gpui::{AppContext, Model, SharedString, Task, TestAppContext, WeakView};
|
||||||
use indoc::indoc;
|
|
||||||
use language::{Buffer, LanguageRegistry, LspAdapterDelegate};
|
use language::{Buffer, LanguageRegistry, LspAdapterDelegate};
|
||||||
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
|
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use rope::Point;
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -27,14 +27,15 @@ use std::{
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::{atomic::AtomicBool, Arc},
|
sync::{atomic::AtomicBool, Arc},
|
||||||
};
|
};
|
||||||
use text::{network::Network, OffsetRangeExt as _, ReplicaId, ToPoint as _};
|
use text::{network::Network, OffsetRangeExt as _, ReplicaId};
|
||||||
use ui::{Context as _, WindowContext};
|
use ui::{Context as _, WindowContext};
|
||||||
use unindent::Unindent;
|
use unindent::Unindent;
|
||||||
use util::{test::marked_text_ranges, RandomCharIter};
|
use util::{
|
||||||
|
test::{generate_marked_text, marked_text_ranges},
|
||||||
|
RandomCharIter,
|
||||||
|
};
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
use super::MessageCacheMetadata;
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
|
@ -479,28 +480,12 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||||
cx.update(prompt_library::init);
|
cx.update(prompt_library::init);
|
||||||
let settings_store = cx.update(SettingsStore::test);
|
let settings_store = cx.update(SettingsStore::test);
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
|
cx.update(language::init);
|
||||||
cx.update(Project::init_settings);
|
cx.update(Project::init_settings);
|
||||||
let fs = FakeFs::new(cx.executor());
|
let fs = FakeFs::new(cx.executor());
|
||||||
fs.as_fake()
|
|
||||||
.insert_tree(
|
|
||||||
"/root",
|
|
||||||
json!({
|
|
||||||
"hello.rs": r#"
|
|
||||||
fn hello() {
|
|
||||||
println!("Hello, World!");
|
|
||||||
}
|
|
||||||
"#.unindent()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
let project = Project::test(fs, [Path::new("/root")], cx).await;
|
let project = Project::test(fs, [Path::new("/root")], cx).await;
|
||||||
cx.update(LanguageModelRegistry::test);
|
cx.update(LanguageModelRegistry::test);
|
||||||
|
|
||||||
let model = cx.read(|cx| {
|
|
||||||
LanguageModelRegistry::read_global(cx)
|
|
||||||
.active_model()
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
cx.update(assistant_panel::init);
|
cx.update(assistant_panel::init);
|
||||||
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||||
|
|
||||||
|
@ -515,151 +500,382 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
let buffer = context.read_with(cx, |context, _| context.buffer.clone());
|
|
||||||
|
|
||||||
// Simulate user input
|
// Insert an assistant message to simulate a response.
|
||||||
let user_message = indoc! {r#"
|
let assistant_message_id = context.update(cx, |context, cx| {
|
||||||
Please add unnecessary complexity to this code:
|
let user_message_id = context.messages(cx).next().unwrap().id;
|
||||||
|
|
||||||
```hello.rs
|
|
||||||
fn main() {
|
|
||||||
println!("Hello, World!");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
"#};
|
|
||||||
buffer.update(cx, |buffer, cx| {
|
|
||||||
buffer.edit([(0..0, user_message)], None, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Simulate LLM response with edit steps
|
|
||||||
let llm_response = indoc! {r#"
|
|
||||||
Sure, I can help you with that. Here's a step-by-step process:
|
|
||||||
|
|
||||||
<step>
|
|
||||||
First, let's extract the greeting into a separate function:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn greet() {
|
|
||||||
println!("Hello, World!");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
greet();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</step>
|
|
||||||
|
|
||||||
<step>
|
|
||||||
Now, let's make the greeting customizable:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn greet(name: &str) {
|
|
||||||
println!("Hello, {}!", name);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
greet("World");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</step>
|
|
||||||
|
|
||||||
These changes make the code more modular and flexible.
|
|
||||||
"#};
|
|
||||||
|
|
||||||
// Simulate the assist method to trigger the LLM response
|
|
||||||
context.update(cx, |context, cx| context.assist(cx));
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
// Retrieve the assistant response message's start from the context
|
|
||||||
let response_start_row = context.read_with(cx, |context, cx| {
|
|
||||||
let buffer = context.buffer.read(cx);
|
|
||||||
context.message_anchors[1].start.to_point(buffer).row
|
|
||||||
});
|
|
||||||
|
|
||||||
// Simulate the LLM completion
|
|
||||||
model
|
|
||||||
.as_fake()
|
|
||||||
.stream_last_completion_response(llm_response.to_string());
|
|
||||||
model.as_fake().end_last_completion_stream();
|
|
||||||
|
|
||||||
// Wait for the completion to be processed
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
// Verify that the edit steps were parsed correctly
|
|
||||||
context.read_with(cx, |context, cx| {
|
|
||||||
assert_eq!(
|
|
||||||
workflow_steps(context, cx),
|
|
||||||
vec![
|
|
||||||
(
|
|
||||||
Point::new(response_start_row + 2, 0)..Point::new(response_start_row + 12, 3),
|
|
||||||
WorkflowStepTestStatus::Pending
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Point::new(response_start_row + 14, 0)..Point::new(response_start_row + 24, 3),
|
|
||||||
WorkflowStepTestStatus::Pending
|
|
||||||
),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
model
|
|
||||||
.as_fake()
|
|
||||||
.respond_to_last_tool_use(tool::WorkflowStepResolutionTool {
|
|
||||||
step_title: "Title".into(),
|
|
||||||
suggestions: vec![tool::WorkflowSuggestionTool {
|
|
||||||
path: "/root/hello.rs".into(),
|
|
||||||
// Simulate a symbol name that's slightly different than our outline query
|
|
||||||
kind: tool::WorkflowSuggestionToolKind::Update {
|
|
||||||
symbol: "fn main()".into(),
|
|
||||||
description: "Extract a greeting function".into(),
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for tool use to be processed.
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
// Verify that the first edit step is not pending anymore.
|
|
||||||
context.read_with(cx, |context, cx| {
|
|
||||||
assert_eq!(
|
|
||||||
workflow_steps(context, cx),
|
|
||||||
vec![
|
|
||||||
(
|
|
||||||
Point::new(response_start_row + 2, 0)..Point::new(response_start_row + 12, 3),
|
|
||||||
WorkflowStepTestStatus::Resolved
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Point::new(response_start_row + 14, 0)..Point::new(response_start_row + 24, 3),
|
|
||||||
WorkflowStepTestStatus::Pending
|
|
||||||
),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
|
||||||
enum WorkflowStepTestStatus {
|
|
||||||
Pending,
|
|
||||||
Resolved,
|
|
||||||
Error,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn workflow_steps(
|
|
||||||
context: &Context,
|
|
||||||
cx: &AppContext,
|
|
||||||
) -> Vec<(Range<Point>, WorkflowStepTestStatus)> {
|
|
||||||
context
|
context
|
||||||
|
.insert_message_after(user_message_id, Role::Assistant, MessageStatus::Done, cx)
|
||||||
|
.unwrap()
|
||||||
|
.id
|
||||||
|
});
|
||||||
|
|
||||||
|
// No edit tags
|
||||||
|
edit(
|
||||||
|
&context,
|
||||||
|
"
|
||||||
|
|
||||||
|
«one
|
||||||
|
two
|
||||||
|
»",
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
expect_steps(
|
||||||
|
&context,
|
||||||
|
"
|
||||||
|
|
||||||
|
one
|
||||||
|
two
|
||||||
|
",
|
||||||
|
&[],
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Partial edit step tag is added
|
||||||
|
edit(
|
||||||
|
&context,
|
||||||
|
"
|
||||||
|
|
||||||
|
one
|
||||||
|
two
|
||||||
|
«
|
||||||
|
<step»",
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
expect_steps(
|
||||||
|
&context,
|
||||||
|
"
|
||||||
|
|
||||||
|
one
|
||||||
|
two
|
||||||
|
|
||||||
|
<step",
|
||||||
|
&[],
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
// The rest of the step tag is added. The unclosed
|
||||||
|
// step is treated as incomplete.
|
||||||
|
edit(
|
||||||
|
&context,
|
||||||
|
"
|
||||||
|
|
||||||
|
one
|
||||||
|
two
|
||||||
|
|
||||||
|
<step«>
|
||||||
|
Add a second function
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn two() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
<edit>»",
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
expect_steps(
|
||||||
|
&context,
|
||||||
|
"
|
||||||
|
|
||||||
|
one
|
||||||
|
two
|
||||||
|
|
||||||
|
«<step>
|
||||||
|
Add a second function
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn two() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
<edit>»",
|
||||||
|
&[&[]],
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
// The full suggestion is added
|
||||||
|
edit(
|
||||||
|
&context,
|
||||||
|
"
|
||||||
|
|
||||||
|
one
|
||||||
|
two
|
||||||
|
|
||||||
|
<step>
|
||||||
|
Add a second function
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn two() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
<edit>«
|
||||||
|
<path>src/lib.rs</path>
|
||||||
|
<operation>insert_sibling_after</operation>
|
||||||
|
<symbol>fn one</symbol>
|
||||||
|
<description>add a `two` function</description>
|
||||||
|
</edit>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
also,»",
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
expect_steps(
|
||||||
|
&context,
|
||||||
|
"
|
||||||
|
|
||||||
|
one
|
||||||
|
two
|
||||||
|
|
||||||
|
«<step>
|
||||||
|
Add a second function
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn two() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
<edit>
|
||||||
|
<path>src/lib.rs</path>
|
||||||
|
<operation>insert_sibling_after</operation>
|
||||||
|
<symbol>fn one</symbol>
|
||||||
|
<description>add a `two` function</description>
|
||||||
|
</edit>
|
||||||
|
</step>»
|
||||||
|
|
||||||
|
also,",
|
||||||
|
&[&[WorkflowStepEdit {
|
||||||
|
path: "src/lib.rs".into(),
|
||||||
|
kind: WorkflowStepEditKind::InsertSiblingAfter {
|
||||||
|
symbol: "fn one".into(),
|
||||||
|
description: "add a `two` function".into(),
|
||||||
|
},
|
||||||
|
}]],
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
// The step is manually edited.
|
||||||
|
edit(
|
||||||
|
&context,
|
||||||
|
"
|
||||||
|
|
||||||
|
one
|
||||||
|
two
|
||||||
|
|
||||||
|
<step>
|
||||||
|
Add a second function
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn two() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
<edit>
|
||||||
|
<path>src/lib.rs</path>
|
||||||
|
<operation>insert_sibling_after</operation>
|
||||||
|
<symbol>«fn zero»</symbol>
|
||||||
|
<description>add a `two` function</description>
|
||||||
|
</edit>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
also,",
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
expect_steps(
|
||||||
|
&context,
|
||||||
|
"
|
||||||
|
|
||||||
|
one
|
||||||
|
two
|
||||||
|
|
||||||
|
«<step>
|
||||||
|
Add a second function
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn two() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
<edit>
|
||||||
|
<path>src/lib.rs</path>
|
||||||
|
<operation>insert_sibling_after</operation>
|
||||||
|
<symbol>fn zero</symbol>
|
||||||
|
<description>add a `two` function</description>
|
||||||
|
</edit>
|
||||||
|
</step>»
|
||||||
|
|
||||||
|
also,",
|
||||||
|
&[&[WorkflowStepEdit {
|
||||||
|
path: "src/lib.rs".into(),
|
||||||
|
kind: WorkflowStepEditKind::InsertSiblingAfter {
|
||||||
|
symbol: "fn zero".into(),
|
||||||
|
description: "add a `two` function".into(),
|
||||||
|
},
|
||||||
|
}]],
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
// When setting the message role to User, the steps are cleared.
|
||||||
|
context.update(cx, |context, cx| {
|
||||||
|
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
|
||||||
|
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
|
||||||
|
});
|
||||||
|
expect_steps(
|
||||||
|
&context,
|
||||||
|
"
|
||||||
|
|
||||||
|
one
|
||||||
|
two
|
||||||
|
|
||||||
|
<step>
|
||||||
|
Add a second function
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn two() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
<edit>
|
||||||
|
<path>src/lib.rs</path>
|
||||||
|
<operation>insert_sibling_after</operation>
|
||||||
|
<symbol>fn zero</symbol>
|
||||||
|
<description>add a `two` function</description>
|
||||||
|
</edit>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
also,",
|
||||||
|
&[],
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
// When setting the message role back to Assistant, the steps are reparsed.
|
||||||
|
context.update(cx, |context, cx| {
|
||||||
|
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
|
||||||
|
});
|
||||||
|
expect_steps(
|
||||||
|
&context,
|
||||||
|
"
|
||||||
|
|
||||||
|
one
|
||||||
|
two
|
||||||
|
|
||||||
|
«<step>
|
||||||
|
Add a second function
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn two() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
<edit>
|
||||||
|
<path>src/lib.rs</path>
|
||||||
|
<operation>insert_sibling_after</operation>
|
||||||
|
<symbol>fn zero</symbol>
|
||||||
|
<description>add a `two` function</description>
|
||||||
|
</edit>
|
||||||
|
</step>»
|
||||||
|
|
||||||
|
also,",
|
||||||
|
&[&[WorkflowStepEdit {
|
||||||
|
path: "src/lib.rs".into(),
|
||||||
|
kind: WorkflowStepEditKind::InsertSiblingAfter {
|
||||||
|
symbol: "fn zero".into(),
|
||||||
|
description: "add a `two` function".into(),
|
||||||
|
},
|
||||||
|
}]],
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure steps are re-parsed when deserializing.
|
||||||
|
let serialized_context = context.read_with(cx, |context, cx| context.serialize(cx));
|
||||||
|
let deserialized_context = cx.new_model(|cx| {
|
||||||
|
Context::deserialize(
|
||||||
|
serialized_context,
|
||||||
|
Default::default(),
|
||||||
|
registry.clone(),
|
||||||
|
prompt_builder.clone(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
expect_steps(
|
||||||
|
&deserialized_context,
|
||||||
|
"
|
||||||
|
|
||||||
|
one
|
||||||
|
two
|
||||||
|
|
||||||
|
«<step>
|
||||||
|
Add a second function
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn two() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
<edit>
|
||||||
|
<path>src/lib.rs</path>
|
||||||
|
<operation>insert_sibling_after</operation>
|
||||||
|
<symbol>fn zero</symbol>
|
||||||
|
<description>add a `two` function</description>
|
||||||
|
</edit>
|
||||||
|
</step>»
|
||||||
|
|
||||||
|
also,",
|
||||||
|
&[&[WorkflowStepEdit {
|
||||||
|
path: "src/lib.rs".into(),
|
||||||
|
kind: WorkflowStepEditKind::InsertSiblingAfter {
|
||||||
|
symbol: "fn zero".into(),
|
||||||
|
description: "add a `two` function".into(),
|
||||||
|
},
|
||||||
|
}]],
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
fn edit(context: &Model<Context>, new_text_marked_with_edits: &str, cx: &mut TestAppContext) {
|
||||||
|
context.update(cx, |context, cx| {
|
||||||
|
context.buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.edit_via_marked_text(&new_text_marked_with_edits.unindent(), None, cx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect_steps(
|
||||||
|
context: &Model<Context>,
|
||||||
|
expected_marked_text: &str,
|
||||||
|
expected_suggestions: &[&[WorkflowStepEdit]],
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
context.update(cx, |context, cx| {
|
||||||
|
let expected_marked_text = expected_marked_text.unindent();
|
||||||
|
let (expected_text, expected_ranges) = marked_text_ranges(&expected_marked_text, false);
|
||||||
|
context.buffer.read_with(cx, |buffer, _| {
|
||||||
|
assert_eq!(buffer.text(), expected_text);
|
||||||
|
let ranges = context
|
||||||
|
.workflow_steps
|
||||||
|
.iter()
|
||||||
|
.map(|entry| entry.range.to_offset(buffer))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let marked = generate_marked_text(&expected_text, &ranges, false);
|
||||||
|
assert_eq!(
|
||||||
|
marked,
|
||||||
|
expected_marked_text,
|
||||||
|
"unexpected suggestion ranges. actual: {ranges:?}, expected: {expected_ranges:?}"
|
||||||
|
);
|
||||||
|
let suggestions = context
|
||||||
.workflow_steps
|
.workflow_steps
|
||||||
.iter()
|
.iter()
|
||||||
.map(|step| {
|
.map(|step| {
|
||||||
let buffer = context.buffer.read(cx);
|
step.edits
|
||||||
let status = match &step.step.read(cx).resolution {
|
.iter()
|
||||||
None => WorkflowStepTestStatus::Pending,
|
.map(|edit| {
|
||||||
Some(Ok(_)) => WorkflowStepTestStatus::Resolved,
|
let edit = edit.as_ref().unwrap();
|
||||||
Some(Err(_)) => WorkflowStepTestStatus::Error,
|
WorkflowStepEdit {
|
||||||
};
|
path: edit.path.clone(),
|
||||||
(step.range.to_point(buffer), status)
|
kind: edit.kind.clone(),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(suggestions, expected_suggestions);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1200,9 +1200,11 @@ impl InlineAssistStatus {
|
||||||
pub(crate) fn is_pending(&self) -> bool {
|
pub(crate) fn is_pending(&self) -> bool {
|
||||||
matches!(self, Self::Pending)
|
matches!(self, Self::Pending)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_confirmed(&self) -> bool {
|
pub(crate) fn is_confirmed(&self) -> bool {
|
||||||
matches!(self, Self::Confirmed)
|
matches!(self, Self::Confirmed)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_done(&self) -> bool {
|
pub(crate) fn is_done(&self) -> bool {
|
||||||
matches!(self, Self::Done)
|
matches!(self, Self::Done)
|
||||||
}
|
}
|
||||||
|
|
|
@ -297,11 +297,4 @@ impl PromptBuilder {
|
||||||
pub fn generate_workflow_prompt(&self) -> Result<String, RenderError> {
|
pub fn generate_workflow_prompt(&self) -> Result<String, RenderError> {
|
||||||
self.handlebars.lock().render("edit_workflow", &())
|
self.handlebars.lock().render("edit_workflow", &())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_step_resolution_prompt(
|
|
||||||
&self,
|
|
||||||
context: &StepResolutionContext,
|
|
||||||
) -> Result<String, RenderError> {
|
|
||||||
self.handlebars.lock().render("step_resolution", context)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,37 @@
|
||||||
mod step_view;
|
use crate::{AssistantPanel, InlineAssistId, InlineAssistant};
|
||||||
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
use crate::{
|
|
||||||
prompts::StepResolutionContext, AssistantPanel, Context, InlineAssistId, InlineAssistant,
|
|
||||||
};
|
|
||||||
use anyhow::{anyhow, Error, Result};
|
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use futures::future;
|
use gpui::AsyncAppContext;
|
||||||
use gpui::{
|
use gpui::{Model, Task, UpdateGlobal as _, View, WeakView, WindowContext};
|
||||||
Model, ModelContext, Task, UpdateGlobal as _, View, WeakModel, WeakView, WindowContext,
|
use language::{Anchor, Buffer, BufferSnapshot, Outline, OutlineItem, ParseStatus, SymbolPath};
|
||||||
};
|
use project::{Project, ProjectPath};
|
||||||
use language::{Anchor, Buffer, BufferSnapshot, SymbolPath};
|
|
||||||
use language_model::{LanguageModelRegistry, LanguageModelRequestMessage, Role};
|
|
||||||
use project::Project;
|
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use smol::stream::StreamExt;
|
use std::{ops::Range, path::Path, sync::Arc};
|
||||||
use std::{cmp, fmt::Write, ops::Range, sync::Arc};
|
|
||||||
use text::{AnchorRangeExt as _, OffsetRangeExt as _};
|
|
||||||
use util::ResultExt as _;
|
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
pub use step_view::WorkflowStepView;
|
|
||||||
|
|
||||||
const IMPORTS_SYMBOL: &str = "#imports";
|
const IMPORTS_SYMBOL: &str = "#imports";
|
||||||
|
|
||||||
pub struct WorkflowStep {
|
#[derive(Debug)]
|
||||||
context: WeakModel<Context>,
|
pub(crate) struct WorkflowStep {
|
||||||
context_buffer_range: Range<Anchor>,
|
pub range: Range<language::Anchor>,
|
||||||
tool_output: String,
|
pub leading_tags_end: text::Anchor,
|
||||||
resolve_task: Option<Task<()>>,
|
pub trailing_tag_start: Option<text::Anchor>,
|
||||||
pub resolution: Option<Result<WorkflowStepResolution, Arc<Error>>>,
|
pub edits: Arc<[Result<WorkflowStepEdit>]>,
|
||||||
|
pub resolution_task: Option<Task<()>>,
|
||||||
|
pub resolution: Option<Arc<Result<WorkflowStepResolution>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) struct WorkflowStepEdit {
|
||||||
|
pub path: String,
|
||||||
|
pub kind: WorkflowStepEditKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct WorkflowStepResolution {
|
pub(crate) struct WorkflowStepResolution {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub suggestion_groups: HashMap<Model<Buffer>, Vec<WorkflowSuggestionGroup>>,
|
pub suggestion_groups: HashMap<Model<Buffer>, Vec<WorkflowSuggestionGroup>>,
|
||||||
}
|
}
|
||||||
|
@ -81,194 +78,6 @@ pub enum WorkflowSuggestion {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WorkflowStep {
|
|
||||||
pub fn new(range: Range<Anchor>, context: WeakModel<Context>) -> Self {
|
|
||||||
Self {
|
|
||||||
context_buffer_range: range,
|
|
||||||
tool_output: String::new(),
|
|
||||||
context,
|
|
||||||
resolution: None,
|
|
||||||
resolve_task: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resolve(&mut self, cx: &mut ModelContext<WorkflowStep>) -> Option<()> {
|
|
||||||
let range = self.context_buffer_range.clone();
|
|
||||||
let context = self.context.upgrade()?;
|
|
||||||
let context = context.read(cx);
|
|
||||||
let project = context.project()?;
|
|
||||||
let prompt_builder = context.prompt_builder();
|
|
||||||
let mut request = context.to_completion_request(cx);
|
|
||||||
let model = LanguageModelRegistry::read_global(cx).active_model();
|
|
||||||
let context_buffer = context.buffer();
|
|
||||||
let step_text = context_buffer
|
|
||||||
.read(cx)
|
|
||||||
.text_for_range(range.clone())
|
|
||||||
.collect::<String>();
|
|
||||||
|
|
||||||
let mut workflow_context = String::new();
|
|
||||||
for message in context.messages(cx) {
|
|
||||||
write!(&mut workflow_context, "<message role={}>", message.role).unwrap();
|
|
||||||
for chunk in context_buffer.read(cx).text_for_range(message.offset_range) {
|
|
||||||
write!(&mut workflow_context, "{chunk}").unwrap();
|
|
||||||
}
|
|
||||||
write!(&mut workflow_context, "</message>").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.resolve_task = Some(cx.spawn(|this, mut cx| async move {
|
|
||||||
let result = async {
|
|
||||||
let Some(model) = model else {
|
|
||||||
return Err(anyhow!("no model selected"));
|
|
||||||
};
|
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
this.tool_output.clear();
|
|
||||||
this.resolution = None;
|
|
||||||
this.result_updated(cx);
|
|
||||||
cx.notify();
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let resolution_context = StepResolutionContext {
|
|
||||||
workflow_context,
|
|
||||||
step_to_resolve: step_text.clone(),
|
|
||||||
};
|
|
||||||
let mut prompt =
|
|
||||||
prompt_builder.generate_step_resolution_prompt(&resolution_context)?;
|
|
||||||
prompt.push_str(&step_text);
|
|
||||||
request.messages.push(LanguageModelRequestMessage {
|
|
||||||
role: Role::User,
|
|
||||||
content: vec![prompt.into()],
|
|
||||||
cache: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Invoke the model to get its edit suggestions for this workflow step.
|
|
||||||
let mut stream = model
|
|
||||||
.use_tool_stream::<tool::WorkflowStepResolutionTool>(request, &cx)
|
|
||||||
.await?;
|
|
||||||
while let Some(chunk) = stream.next().await {
|
|
||||||
let chunk = chunk?;
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
this.tool_output.push_str(&chunk);
|
|
||||||
cx.notify();
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let resolution = this.update(&mut cx, |this, _| {
|
|
||||||
serde_json::from_str::<tool::WorkflowStepResolutionTool>(&this.tool_output)
|
|
||||||
})??;
|
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
this.tool_output = serde_json::to_string_pretty(&resolution).unwrap();
|
|
||||||
cx.notify();
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Translate the parsed suggestions to our internal types, which anchor the suggestions to locations in the code.
|
|
||||||
let suggestion_tasks: Vec<_> = resolution
|
|
||||||
.suggestions
|
|
||||||
.iter()
|
|
||||||
.map(|suggestion| suggestion.resolve(project.clone(), cx.clone()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Expand the context ranges of each suggestion and group suggestions with overlapping context ranges.
|
|
||||||
let suggestions = future::join_all(suggestion_tasks)
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|task| task.log_err())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let mut suggestions_by_buffer = HashMap::default();
|
|
||||||
for (buffer, suggestion) in suggestions {
|
|
||||||
suggestions_by_buffer
|
|
||||||
.entry(buffer)
|
|
||||||
.or_insert_with(Vec::new)
|
|
||||||
.push(suggestion);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut suggestion_groups_by_buffer = HashMap::default();
|
|
||||||
for (buffer, mut suggestions) in suggestions_by_buffer {
|
|
||||||
let mut suggestion_groups = Vec::<WorkflowSuggestionGroup>::new();
|
|
||||||
let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
|
|
||||||
// Sort suggestions by their range so that earlier, larger ranges come first
|
|
||||||
suggestions.sort_by(|a, b| a.range().cmp(&b.range(), &snapshot));
|
|
||||||
|
|
||||||
// Merge overlapping suggestions
|
|
||||||
suggestions.dedup_by(|a, b| b.try_merge(a, &snapshot));
|
|
||||||
|
|
||||||
// Create context ranges for each suggestion
|
|
||||||
for suggestion in suggestions {
|
|
||||||
let context_range = {
|
|
||||||
let suggestion_point_range = suggestion.range().to_point(&snapshot);
|
|
||||||
let start_row = suggestion_point_range.start.row.saturating_sub(5);
|
|
||||||
let end_row = cmp::min(
|
|
||||||
suggestion_point_range.end.row + 5,
|
|
||||||
snapshot.max_point().row,
|
|
||||||
);
|
|
||||||
let start = snapshot.anchor_before(Point::new(start_row, 0));
|
|
||||||
let end = snapshot
|
|
||||||
.anchor_after(Point::new(end_row, snapshot.line_len(end_row)));
|
|
||||||
start..end
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(last_group) = suggestion_groups.last_mut() {
|
|
||||||
if last_group
|
|
||||||
.context_range
|
|
||||||
.end
|
|
||||||
.cmp(&context_range.start, &snapshot)
|
|
||||||
.is_ge()
|
|
||||||
{
|
|
||||||
// Merge with the previous group if context ranges overlap
|
|
||||||
last_group.context_range.end = context_range.end;
|
|
||||||
last_group.suggestions.push(suggestion);
|
|
||||||
} else {
|
|
||||||
// Create a new group
|
|
||||||
suggestion_groups.push(WorkflowSuggestionGroup {
|
|
||||||
context_range,
|
|
||||||
suggestions: vec![suggestion],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Create the first group
|
|
||||||
suggestion_groups.push(WorkflowSuggestionGroup {
|
|
||||||
context_range,
|
|
||||||
suggestions: vec![suggestion],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suggestion_groups_by_buffer.insert(buffer, suggestion_groups);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((resolution.step_title, suggestion_groups_by_buffer))
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = result.await;
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
this.resolution = Some(match result {
|
|
||||||
Ok((title, suggestion_groups)) => Ok(WorkflowStepResolution {
|
|
||||||
title,
|
|
||||||
suggestion_groups,
|
|
||||||
}),
|
|
||||||
Err(error) => Err(Arc::new(error)),
|
|
||||||
});
|
|
||||||
this.context
|
|
||||||
.update(cx, |context, cx| context.workflow_step_updated(range, cx))
|
|
||||||
.ok();
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}));
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn result_updated(&mut self, cx: &mut ModelContext<Self>) {
|
|
||||||
self.context
|
|
||||||
.update(cx, |context, cx| {
|
|
||||||
context.workflow_step_updated(self.context_buffer_range.clone(), cx)
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WorkflowSuggestion {
|
impl WorkflowSuggestion {
|
||||||
pub fn range(&self) -> Range<language::Anchor> {
|
pub fn range(&self) -> Range<language::Anchor> {
|
||||||
match self {
|
match self {
|
||||||
|
@ -306,31 +115,7 @@ impl WorkflowSuggestion {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn symbol_path(&self) -> Option<&SymbolPath> {
|
pub fn try_merge(&mut self, other: &Self, buffer: &BufferSnapshot) -> bool {
|
||||||
match self {
|
|
||||||
Self::Update { symbol_path, .. } => Some(symbol_path),
|
|
||||||
Self::InsertSiblingBefore { symbol_path, .. } => Some(symbol_path),
|
|
||||||
Self::InsertSiblingAfter { symbol_path, .. } => Some(symbol_path),
|
|
||||||
Self::PrependChild { symbol_path, .. } => symbol_path.as_ref(),
|
|
||||||
Self::AppendChild { symbol_path, .. } => symbol_path.as_ref(),
|
|
||||||
Self::Delete { symbol_path, .. } => Some(symbol_path),
|
|
||||||
Self::CreateFile { .. } => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn kind(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Self::Update { .. } => "Update",
|
|
||||||
Self::CreateFile { .. } => "CreateFile",
|
|
||||||
Self::InsertSiblingBefore { .. } => "InsertSiblingBefore",
|
|
||||||
Self::InsertSiblingAfter { .. } => "InsertSiblingAfter",
|
|
||||||
Self::PrependChild { .. } => "PrependChild",
|
|
||||||
Self::AppendChild { .. } => "AppendChild",
|
|
||||||
Self::Delete { .. } => "Delete",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn try_merge(&mut self, other: &Self, buffer: &BufferSnapshot) -> bool {
|
|
||||||
let range = self.range();
|
let range = self.range();
|
||||||
let other_range = other.range();
|
let other_range = other.range();
|
||||||
|
|
||||||
|
@ -465,63 +250,50 @@ impl WorkflowSuggestion {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod tool {
|
impl WorkflowStepEdit {
|
||||||
use super::*;
|
pub fn new(
|
||||||
use anyhow::Context as _;
|
path: Option<String>,
|
||||||
use gpui::AsyncAppContext;
|
operation: Option<String>,
|
||||||
use language::{Outline, OutlineItem, ParseStatus};
|
symbol: Option<String>,
|
||||||
use language_model::LanguageModelTool;
|
description: Option<String>,
|
||||||
use project::ProjectPath;
|
) -> Result<Self> {
|
||||||
use schemars::JsonSchema;
|
let path = path.ok_or_else(|| anyhow!("missing path"))?;
|
||||||
use std::path::Path;
|
let operation = operation.ok_or_else(|| anyhow!("missing operation"))?;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
let kind = match operation.as_str() {
|
||||||
pub struct WorkflowStepResolutionTool {
|
"update" => WorkflowStepEditKind::Update {
|
||||||
/// An extremely short title for the edit step represented by these operations.
|
symbol: symbol.ok_or_else(|| anyhow!("missing symbol"))?,
|
||||||
pub step_title: String,
|
description: description.ok_or_else(|| anyhow!("missing description"))?,
|
||||||
/// A sequence of operations to apply to the codebase.
|
},
|
||||||
/// When multiple operations are required for a step, be sure to include multiple operations in this list.
|
"insert_sibling_before" => WorkflowStepEditKind::InsertSiblingBefore {
|
||||||
pub suggestions: Vec<WorkflowSuggestionTool>,
|
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"))?,
|
||||||
|
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,
|
||||||
|
description: description.ok_or_else(|| anyhow!("missing description"))?,
|
||||||
|
},
|
||||||
|
"delete" => WorkflowStepEditKind::Delete {
|
||||||
|
symbol: symbol.ok_or_else(|| anyhow!("missing symbol"))?,
|
||||||
|
},
|
||||||
|
"create" => WorkflowStepEditKind::Create {
|
||||||
|
description: description.ok_or_else(|| anyhow!("missing description"))?,
|
||||||
|
},
|
||||||
|
_ => Err(anyhow!("unknown operation {operation:?}"))?,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self { path, kind })
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LanguageModelTool for WorkflowStepResolutionTool {
|
pub async fn resolve(
|
||||||
fn name() -> String {
|
|
||||||
"edit".into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description() -> String {
|
|
||||||
"suggest edits to one or more locations in the codebase".into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A description of an operation to apply to one location in the codebase.
|
|
||||||
///
|
|
||||||
/// This object represents a single edit operation that can be performed on a specific file
|
|
||||||
/// in the codebase. It encapsulates both the location (file path) and the nature of the
|
|
||||||
/// edit to be made.
|
|
||||||
///
|
|
||||||
/// # Fields
|
|
||||||
///
|
|
||||||
/// * `path`: A string representing the file path where the edit operation should be applied.
|
|
||||||
/// This path is relative to the root of the project or repository.
|
|
||||||
///
|
|
||||||
/// * `kind`: An enum representing the specific type of edit operation to be performed.
|
|
||||||
///
|
|
||||||
/// # Usage
|
|
||||||
///
|
|
||||||
/// `EditOperation` is used within a code editor to represent and apply
|
|
||||||
/// programmatic changes to source code. It provides a structured way to describe
|
|
||||||
/// edits for features like refactoring tools or AI-assisted coding suggestions.
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
|
||||||
pub struct WorkflowSuggestionTool {
|
|
||||||
/// The path to the file containing the relevant operation
|
|
||||||
pub path: String,
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub kind: WorkflowSuggestionToolKind,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WorkflowSuggestionTool {
|
|
||||||
pub(super) async fn resolve(
|
|
||||||
&self,
|
&self,
|
||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
mut cx: AsyncAppContext,
|
mut cx: AsyncAppContext,
|
||||||
|
@ -560,7 +332,7 @@ pub mod tool {
|
||||||
let outline = snapshot.outline(None).context("no outline for buffer")?;
|
let outline = snapshot.outline(None).context("no outline for buffer")?;
|
||||||
|
|
||||||
let suggestion = match kind {
|
let suggestion = match kind {
|
||||||
WorkflowSuggestionToolKind::Update {
|
WorkflowStepEditKind::Update {
|
||||||
symbol,
|
symbol,
|
||||||
description,
|
description,
|
||||||
} => {
|
} => {
|
||||||
|
@ -580,10 +352,10 @@ pub mod tool {
|
||||||
symbol_path,
|
symbol_path,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WorkflowSuggestionToolKind::Create { description } => {
|
WorkflowStepEditKind::Create { description } => {
|
||||||
WorkflowSuggestion::CreateFile { description }
|
WorkflowSuggestion::CreateFile { description }
|
||||||
}
|
}
|
||||||
WorkflowSuggestionToolKind::InsertSiblingBefore {
|
WorkflowStepEditKind::InsertSiblingBefore {
|
||||||
symbol,
|
symbol,
|
||||||
description,
|
description,
|
||||||
} => {
|
} => {
|
||||||
|
@ -601,7 +373,7 @@ pub mod tool {
|
||||||
symbol_path,
|
symbol_path,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WorkflowSuggestionToolKind::InsertSiblingAfter {
|
WorkflowStepEditKind::InsertSiblingAfter {
|
||||||
symbol,
|
symbol,
|
||||||
description,
|
description,
|
||||||
} => {
|
} => {
|
||||||
|
@ -613,13 +385,12 @@ pub mod tool {
|
||||||
symbol_path,
|
symbol_path,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WorkflowSuggestionToolKind::PrependChild {
|
WorkflowStepEditKind::PrependChild {
|
||||||
symbol,
|
symbol,
|
||||||
description,
|
description,
|
||||||
} => {
|
} => {
|
||||||
if let Some(symbol) = symbol {
|
if let Some(symbol) = symbol {
|
||||||
let (symbol_path, symbol) =
|
let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?;
|
||||||
Self::resolve_symbol(&snapshot, &outline, &symbol)?;
|
|
||||||
|
|
||||||
let position = snapshot.anchor_after(
|
let position = snapshot.anchor_after(
|
||||||
symbol
|
symbol
|
||||||
|
@ -639,13 +410,12 @@ pub mod tool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WorkflowSuggestionToolKind::AppendChild {
|
WorkflowStepEditKind::AppendChild {
|
||||||
symbol,
|
symbol,
|
||||||
description,
|
description,
|
||||||
} => {
|
} => {
|
||||||
if let Some(symbol) = symbol {
|
if let Some(symbol) = symbol {
|
||||||
let (symbol_path, symbol) =
|
let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?;
|
||||||
Self::resolve_symbol(&snapshot, &outline, &symbol)?;
|
|
||||||
|
|
||||||
let position = snapshot.anchor_before(
|
let position = snapshot.anchor_before(
|
||||||
symbol
|
symbol
|
||||||
|
@ -665,7 +435,7 @@ pub mod tool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WorkflowSuggestionToolKind::Delete { symbol } => {
|
WorkflowStepEditKind::Delete { symbol } => {
|
||||||
let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?;
|
let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?;
|
||||||
let start = symbol
|
let start = symbol
|
||||||
.annotation_range
|
.annotation_range
|
||||||
|
@ -739,8 +509,8 @@ pub mod tool {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||||
#[serde(tag = "kind")]
|
#[serde(tag = "operation")]
|
||||||
pub enum WorkflowSuggestionToolKind {
|
pub enum WorkflowStepEditKind {
|
||||||
/// Rewrites the specified symbol entirely based on the given description.
|
/// Rewrites the specified symbol entirely based on the given description.
|
||||||
/// This operation completely replaces the existing symbol with new content.
|
/// This operation completely replaces the existing symbol with new content.
|
||||||
Update {
|
Update {
|
||||||
|
@ -800,4 +570,3 @@ pub mod tool {
|
||||||
symbol: String,
|
symbol: String,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -1,315 +0,0 @@
|
||||||
use super::WorkflowStep;
|
|
||||||
use crate::{Assist, Context};
|
|
||||||
use editor::{
|
|
||||||
display_map::{BlockDisposition, BlockProperties, BlockStyle},
|
|
||||||
Editor, EditorEvent, ExcerptRange, MultiBuffer,
|
|
||||||
};
|
|
||||||
use gpui::{
|
|
||||||
div, AnyElement, AppContext, Context as _, Empty, EventEmitter, FocusableView, IntoElement,
|
|
||||||
Model, ParentElement as _, Render, SharedString, Styled as _, View, ViewContext,
|
|
||||||
VisualContext as _, WeakModel, WindowContext,
|
|
||||||
};
|
|
||||||
use language::{language_settings::SoftWrap, Anchor, Buffer, LanguageRegistry};
|
|
||||||
use std::{ops::DerefMut, sync::Arc};
|
|
||||||
use text::OffsetRangeExt;
|
|
||||||
use theme::ActiveTheme as _;
|
|
||||||
use ui::{
|
|
||||||
h_flex, v_flex, ButtonCommon as _, ButtonLike, ButtonStyle, Color, Icon, IconName,
|
|
||||||
InteractiveElement as _, Label, LabelCommon as _,
|
|
||||||
};
|
|
||||||
use workspace::{
|
|
||||||
item::{self, Item},
|
|
||||||
pane,
|
|
||||||
searchable::SearchableItemHandle,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct WorkflowStepView {
|
|
||||||
step: WeakModel<WorkflowStep>,
|
|
||||||
tool_output_buffer: Model<Buffer>,
|
|
||||||
editor: View<Editor>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WorkflowStepView {
|
|
||||||
pub fn new(
|
|
||||||
context: Model<Context>,
|
|
||||||
step: Model<WorkflowStep>,
|
|
||||||
language_registry: Arc<LanguageRegistry>,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) -> Self {
|
|
||||||
let tool_output_buffer =
|
|
||||||
cx.new_model(|cx| Buffer::local(step.read(cx).tool_output.clone(), cx));
|
|
||||||
let buffer = cx.new_model(|cx| {
|
|
||||||
let mut buffer = MultiBuffer::without_headers(0, language::Capability::ReadWrite);
|
|
||||||
buffer.push_excerpts(
|
|
||||||
context.read(cx).buffer().clone(),
|
|
||||||
[ExcerptRange {
|
|
||||||
context: step.read(cx).context_buffer_range.clone(),
|
|
||||||
primary: None,
|
|
||||||
}],
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
buffer.push_excerpts(
|
|
||||||
tool_output_buffer.clone(),
|
|
||||||
[ExcerptRange {
|
|
||||||
context: Anchor::MIN..Anchor::MAX,
|
|
||||||
primary: None,
|
|
||||||
}],
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
buffer
|
|
||||||
});
|
|
||||||
|
|
||||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
|
||||||
let output_excerpt = buffer_snapshot.excerpts().skip(1).next().unwrap().0;
|
|
||||||
let input_start_anchor = multi_buffer::Anchor::min();
|
|
||||||
let output_start_anchor = buffer_snapshot
|
|
||||||
.anchor_in_excerpt(output_excerpt, Anchor::MIN)
|
|
||||||
.unwrap();
|
|
||||||
let output_end_anchor = multi_buffer::Anchor::max();
|
|
||||||
|
|
||||||
let handle = cx.view().downgrade();
|
|
||||||
let editor = cx.new_view(|cx| {
|
|
||||||
let mut editor = Editor::for_multibuffer(buffer.clone(), None, false, cx);
|
|
||||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
|
||||||
editor.set_show_line_numbers(false, cx);
|
|
||||||
editor.set_show_git_diff_gutter(false, cx);
|
|
||||||
editor.set_show_code_actions(false, cx);
|
|
||||||
editor.set_show_runnables(false, cx);
|
|
||||||
editor.set_show_wrap_guides(false, cx);
|
|
||||||
editor.set_show_indent_guides(false, cx);
|
|
||||||
editor.set_read_only(true);
|
|
||||||
editor.set_show_inline_completions(Some(false), cx);
|
|
||||||
editor.insert_blocks(
|
|
||||||
[
|
|
||||||
BlockProperties {
|
|
||||||
position: input_start_anchor,
|
|
||||||
height: 1,
|
|
||||||
style: BlockStyle::Fixed,
|
|
||||||
render: Box::new(|cx| section_header("Step Input", cx)),
|
|
||||||
disposition: BlockDisposition::Above,
|
|
||||||
priority: 0,
|
|
||||||
},
|
|
||||||
BlockProperties {
|
|
||||||
position: output_start_anchor,
|
|
||||||
height: 1,
|
|
||||||
style: BlockStyle::Fixed,
|
|
||||||
render: Box::new(|cx| section_header("Tool Output", cx)),
|
|
||||||
disposition: BlockDisposition::Above,
|
|
||||||
priority: 0,
|
|
||||||
},
|
|
||||||
BlockProperties {
|
|
||||||
position: output_end_anchor,
|
|
||||||
height: 1,
|
|
||||||
style: BlockStyle::Fixed,
|
|
||||||
render: Box::new(move |cx| {
|
|
||||||
if let Some(result) = handle.upgrade().and_then(|this| {
|
|
||||||
this.update(cx.deref_mut(), |this, cx| this.render_result(cx))
|
|
||||||
}) {
|
|
||||||
v_flex()
|
|
||||||
.child(section_header("Output", cx))
|
|
||||||
.child(
|
|
||||||
div().pl(cx.gutter_dimensions.full_width()).child(result),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
} else {
|
|
||||||
Empty.into_any_element()
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
disposition: BlockDisposition::Below,
|
|
||||||
priority: 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
editor
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.observe(&step, Self::step_updated).detach();
|
|
||||||
cx.observe_release(&step, Self::step_released).detach();
|
|
||||||
|
|
||||||
cx.spawn(|this, mut cx| async move {
|
|
||||||
if let Ok(language) = language_registry.language_for_name("JSON").await {
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
this.tool_output_buffer.update(cx, |buffer, cx| {
|
|
||||||
buffer.set_language(Some(language), cx);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
tool_output_buffer,
|
|
||||||
step: step.downgrade(),
|
|
||||||
editor,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn step(&self) -> &WeakModel<WorkflowStep> {
|
|
||||||
&self.step
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_result(&mut self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
|
|
||||||
let step = self.step.upgrade()?;
|
|
||||||
let result = step.read(cx).resolution.as_ref()?;
|
|
||||||
match result {
|
|
||||||
Ok(result) => {
|
|
||||||
Some(
|
|
||||||
v_flex()
|
|
||||||
.child(result.title.clone())
|
|
||||||
.children(result.suggestion_groups.iter().filter_map(
|
|
||||||
|(buffer, suggestion_groups)| {
|
|
||||||
let buffer = buffer.read(cx);
|
|
||||||
let path = buffer.file().map(|f| f.path());
|
|
||||||
let snapshot = buffer.snapshot();
|
|
||||||
v_flex()
|
|
||||||
.mb_2()
|
|
||||||
.border_b_1()
|
|
||||||
.children(path.map(|path| format!("path: {}", path.display())))
|
|
||||||
.children(suggestion_groups.iter().map(|group| {
|
|
||||||
v_flex().pt_2().pl_2().children(
|
|
||||||
group.suggestions.iter().map(|suggestion| {
|
|
||||||
let range = suggestion.range().to_point(&snapshot);
|
|
||||||
v_flex()
|
|
||||||
.children(
|
|
||||||
suggestion.description().map(|desc| {
|
|
||||||
format!("description: {desc}")
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(format!("kind: {}", suggestion.kind()))
|
|
||||||
.children(suggestion.symbol_path().map(
|
|
||||||
|path| format!("symbol path: {}", path.0),
|
|
||||||
))
|
|
||||||
.child(format!(
|
|
||||||
"lines: {} - {}",
|
|
||||||
range.start.row + 1,
|
|
||||||
range.end.row + 1
|
|
||||||
))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
.into()
|
|
||||||
},
|
|
||||||
))
|
|
||||||
.into_any_element(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Err(error) => Some(format!("{:?}", error).into_any_element()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn step_updated(&mut self, step: Model<WorkflowStep>, cx: &mut ViewContext<Self>) {
|
|
||||||
self.tool_output_buffer.update(cx, |buffer, cx| {
|
|
||||||
let text = step.read(cx).tool_output.clone();
|
|
||||||
buffer.set_text(text, cx);
|
|
||||||
});
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn step_released(&mut self, _: &mut WorkflowStep, cx: &mut ViewContext<Self>) {
|
|
||||||
cx.emit(EditorEvent::Closed);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
|
|
||||||
self.step
|
|
||||||
.update(cx, |step, cx| {
|
|
||||||
step.resolve(cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn section_header(
|
|
||||||
name: &'static str,
|
|
||||||
cx: &mut editor::display_map::BlockContext,
|
|
||||||
) -> gpui::AnyElement {
|
|
||||||
h_flex()
|
|
||||||
.pl(cx.gutter_dimensions.full_width())
|
|
||||||
.h_11()
|
|
||||||
.w_full()
|
|
||||||
.relative()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
ButtonLike::new("role")
|
|
||||||
.style(ButtonStyle::Filled)
|
|
||||||
.child(Label::new(name).color(Color::Default)),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for WorkflowStepView {
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
|
||||||
div()
|
|
||||||
.key_context("ContextEditor")
|
|
||||||
.on_action(cx.listener(Self::resolve))
|
|
||||||
.flex_grow()
|
|
||||||
.bg(cx.theme().colors().editor_background)
|
|
||||||
.child(self.editor.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<EditorEvent> for WorkflowStepView {}
|
|
||||||
|
|
||||||
impl FocusableView for WorkflowStepView {
|
|
||||||
fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
|
|
||||||
self.editor.read(cx).focus_handle(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Item for WorkflowStepView {
|
|
||||||
type Event = EditorEvent;
|
|
||||||
|
|
||||||
fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
|
|
||||||
let step = self.step.upgrade()?.read(cx);
|
|
||||||
let context = step.context.upgrade()?.read(cx);
|
|
||||||
let buffer = context.buffer().read(cx);
|
|
||||||
let index = context
|
|
||||||
.workflow_step_index_for_range(&step.context_buffer_range, buffer)
|
|
||||||
.ok()?
|
|
||||||
+ 1;
|
|
||||||
Some(format!("Step {index}").into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tab_icon(&self, _cx: &WindowContext) -> Option<ui::Icon> {
|
|
||||||
Some(Icon::new(IconName::SearchCode))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
|
|
||||||
match event {
|
|
||||||
EditorEvent::Edited { .. } => {
|
|
||||||
f(item::ItemEvent::Edit);
|
|
||||||
}
|
|
||||||
EditorEvent::TitleChanged => {
|
|
||||||
f(item::ItemEvent::UpdateTab);
|
|
||||||
}
|
|
||||||
EditorEvent::Closed => f(item::ItemEvent::CloseItem),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tab_tooltip_text(&self, _cx: &AppContext) -> Option<SharedString> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_searchable(&self, _handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
|
|
||||||
self.editor.update(cx, |editor, cx| {
|
|
||||||
Item::set_nav_history(editor, nav_history, cx)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
|
|
||||||
self.editor
|
|
||||||
.update(cx, |editor, cx| Item::navigate(editor, data, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
|
||||||
self.editor
|
|
||||||
.update(cx, |editor, cx| Item::deactivated(editor, cx))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -105,7 +105,7 @@ pub struct DisplayMap {
|
||||||
inlay_highlights: InlayHighlights,
|
inlay_highlights: InlayHighlights,
|
||||||
/// A container for explicitly foldable ranges, which supersede indentation based fold range suggestions.
|
/// A container for explicitly foldable ranges, which supersede indentation based fold range suggestions.
|
||||||
crease_map: CreaseMap,
|
crease_map: CreaseMap,
|
||||||
fold_placeholder: FoldPlaceholder,
|
pub(crate) fold_placeholder: FoldPlaceholder,
|
||||||
pub clip_at_line_ends: bool,
|
pub clip_at_line_ends: bool,
|
||||||
pub(crate) masked: bool,
|
pub(crate) masked: bool,
|
||||||
}
|
}
|
||||||
|
|
|
@ -10388,6 +10388,10 @@ impl Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn default_fold_placeholder(&self, cx: &AppContext) -> FoldPlaceholder {
|
||||||
|
self.display_map.read(cx).fold_placeholder.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_gutter_hovered(&mut self, hovered: bool, cx: &mut ViewContext<Self>) {
|
pub fn set_gutter_hovered(&mut self, hovered: bool, cx: &mut ViewContext<Self>) {
|
||||||
if hovered != self.gutter_hovered {
|
if hovered != self.gutter_hovered {
|
||||||
self.gutter_hovered = hovered;
|
self.gutter_hovered = hovered;
|
||||||
|
|
|
@ -41,8 +41,12 @@ use gpui::{
|
||||||
ViewContext, WeakView, WindowContext,
|
ViewContext, WeakView, WindowContext,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::language_settings::{
|
use language::{
|
||||||
IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings, ShowWhitespaceSetting,
|
language_settings::{
|
||||||
|
IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings,
|
||||||
|
ShowWhitespaceSetting,
|
||||||
|
},
|
||||||
|
ChunkRendererContext,
|
||||||
};
|
};
|
||||||
use lsp::DiagnosticSeverity;
|
use lsp::DiagnosticSeverity;
|
||||||
use multi_buffer::{Anchor, MultiBufferPoint, MultiBufferRow};
|
use multi_buffer::{Anchor, MultiBufferPoint, MultiBufferRow};
|
||||||
|
@ -1872,6 +1876,7 @@ impl EditorElement {
|
||||||
line_number_layouts: &[Option<ShapedLine>],
|
line_number_layouts: &[Option<ShapedLine>],
|
||||||
snapshot: &EditorSnapshot,
|
snapshot: &EditorSnapshot,
|
||||||
style: &EditorStyle,
|
style: &EditorStyle,
|
||||||
|
editor_width: Pixels,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Vec<LineWithInvisibles> {
|
) -> Vec<LineWithInvisibles> {
|
||||||
if rows.start >= rows.end {
|
if rows.start >= rows.end {
|
||||||
|
@ -1922,6 +1927,7 @@ impl EditorElement {
|
||||||
rows.len(),
|
rows.len(),
|
||||||
line_number_layouts,
|
line_number_layouts,
|
||||||
snapshot.mode,
|
snapshot.mode,
|
||||||
|
editor_width,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1966,6 +1972,7 @@ impl EditorElement {
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
em_width: Pixels,
|
em_width: Pixels,
|
||||||
text_hitbox: &Hitbox,
|
text_hitbox: &Hitbox,
|
||||||
|
editor_width: Pixels,
|
||||||
scroll_width: &mut Pixels,
|
scroll_width: &mut Pixels,
|
||||||
resized_blocks: &mut HashMap<CustomBlockId, u32>,
|
resized_blocks: &mut HashMap<CustomBlockId, u32>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
|
@ -1981,7 +1988,7 @@ impl EditorElement {
|
||||||
line_layouts[align_to.row().minus(rows.start) as usize]
|
line_layouts[align_to.row().minus(rows.start) as usize]
|
||||||
.x_for_index(align_to.column() as usize)
|
.x_for_index(align_to.column() as usize)
|
||||||
} else {
|
} else {
|
||||||
layout_line(align_to.row(), snapshot, &self.style, cx)
|
layout_line(align_to.row(), snapshot, &self.style, editor_width, cx)
|
||||||
.x_for_index(align_to.column() as usize)
|
.x_for_index(align_to.column() as usize)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2418,6 +2425,7 @@ impl EditorElement {
|
||||||
snapshot: &EditorSnapshot,
|
snapshot: &EditorSnapshot,
|
||||||
hitbox: &Hitbox,
|
hitbox: &Hitbox,
|
||||||
text_hitbox: &Hitbox,
|
text_hitbox: &Hitbox,
|
||||||
|
editor_width: Pixels,
|
||||||
scroll_width: &mut Pixels,
|
scroll_width: &mut Pixels,
|
||||||
gutter_dimensions: &GutterDimensions,
|
gutter_dimensions: &GutterDimensions,
|
||||||
em_width: Pixels,
|
em_width: Pixels,
|
||||||
|
@ -2457,6 +2465,7 @@ impl EditorElement {
|
||||||
line_height,
|
line_height,
|
||||||
em_width,
|
em_width,
|
||||||
text_hitbox,
|
text_hitbox,
|
||||||
|
editor_width,
|
||||||
scroll_width,
|
scroll_width,
|
||||||
&mut resized_blocks,
|
&mut resized_blocks,
|
||||||
cx,
|
cx,
|
||||||
|
@ -2500,6 +2509,7 @@ impl EditorElement {
|
||||||
line_height,
|
line_height,
|
||||||
em_width,
|
em_width,
|
||||||
text_hitbox,
|
text_hitbox,
|
||||||
|
editor_width,
|
||||||
scroll_width,
|
scroll_width,
|
||||||
&mut resized_blocks,
|
&mut resized_blocks,
|
||||||
cx,
|
cx,
|
||||||
|
@ -2544,6 +2554,7 @@ impl EditorElement {
|
||||||
line_height,
|
line_height,
|
||||||
em_width,
|
em_width,
|
||||||
text_hitbox,
|
text_hitbox,
|
||||||
|
editor_width,
|
||||||
scroll_width,
|
scroll_width,
|
||||||
&mut resized_blocks,
|
&mut resized_blocks,
|
||||||
cx,
|
cx,
|
||||||
|
@ -4353,6 +4364,7 @@ impl fmt::Debug for LineFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LineWithInvisibles {
|
impl LineWithInvisibles {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn from_chunks<'a>(
|
fn from_chunks<'a>(
|
||||||
chunks: impl Iterator<Item = HighlightedChunk<'a>>,
|
chunks: impl Iterator<Item = HighlightedChunk<'a>>,
|
||||||
text_style: &TextStyle,
|
text_style: &TextStyle,
|
||||||
|
@ -4360,6 +4372,7 @@ impl LineWithInvisibles {
|
||||||
max_line_count: usize,
|
max_line_count: usize,
|
||||||
line_number_layouts: &[Option<ShapedLine>],
|
line_number_layouts: &[Option<ShapedLine>],
|
||||||
editor_mode: EditorMode,
|
editor_mode: EditorMode,
|
||||||
|
text_width: Pixels,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Vec<Self> {
|
) -> Vec<Self> {
|
||||||
let mut layouts = Vec::with_capacity(max_line_count);
|
let mut layouts = Vec::with_capacity(max_line_count);
|
||||||
|
@ -4414,7 +4427,10 @@ impl LineWithInvisibles {
|
||||||
AvailableSpace::MinContent
|
AvailableSpace::MinContent
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut element = (renderer.render)(cx);
|
let mut element = (renderer.render)(&mut ChunkRendererContext {
|
||||||
|
context: cx,
|
||||||
|
max_width: text_width,
|
||||||
|
});
|
||||||
let line_height = text_style.line_height_in_pixels(cx.rem_size());
|
let line_height = text_style.line_height_in_pixels(cx.rem_size());
|
||||||
let size = element.layout_as_root(
|
let size = element.layout_as_root(
|
||||||
size(available_width, AvailableSpace::Definite(line_height)),
|
size(available_width, AvailableSpace::Definite(line_height)),
|
||||||
|
@ -4871,6 +4887,7 @@ impl Element for EditorElement {
|
||||||
&[],
|
&[],
|
||||||
&editor_snapshot,
|
&editor_snapshot,
|
||||||
&style,
|
&style,
|
||||||
|
px(f32::MAX),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.pop()
|
.pop()
|
||||||
|
@ -4983,6 +5000,9 @@ impl Element for EditorElement {
|
||||||
};
|
};
|
||||||
let overscroll = size(em_width + right_margin, px(0.));
|
let overscroll = size(em_width + right_margin, px(0.));
|
||||||
|
|
||||||
|
let editor_width =
|
||||||
|
text_width - gutter_dimensions.margin - overscroll.width - em_width;
|
||||||
|
|
||||||
snapshot = self.editor.update(cx, |editor, cx| {
|
snapshot = self.editor.update(cx, |editor, cx| {
|
||||||
editor.last_bounds = Some(bounds);
|
editor.last_bounds = Some(bounds);
|
||||||
editor.gutter_dimensions = gutter_dimensions;
|
editor.gutter_dimensions = gutter_dimensions;
|
||||||
|
@ -4991,8 +5011,6 @@ impl Element for EditorElement {
|
||||||
if matches!(editor.mode, EditorMode::AutoHeight { .. }) {
|
if matches!(editor.mode, EditorMode::AutoHeight { .. }) {
|
||||||
snapshot
|
snapshot
|
||||||
} else {
|
} else {
|
||||||
let editor_width =
|
|
||||||
text_width - gutter_dimensions.margin - overscroll.width - em_width;
|
|
||||||
let wrap_width = match editor.soft_wrap_mode(cx) {
|
let wrap_width = match editor.soft_wrap_mode(cx) {
|
||||||
SoftWrap::None => None,
|
SoftWrap::None => None,
|
||||||
SoftWrap::PreferLine => {
|
SoftWrap::PreferLine => {
|
||||||
|
@ -5162,6 +5180,7 @@ impl Element for EditorElement {
|
||||||
&line_numbers,
|
&line_numbers,
|
||||||
&snapshot,
|
&snapshot,
|
||||||
&self.style,
|
&self.style,
|
||||||
|
editor_width,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
for line_with_invisibles in &line_layouts {
|
for line_with_invisibles in &line_layouts {
|
||||||
|
@ -5171,7 +5190,8 @@ impl Element for EditorElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
let longest_line_width =
|
let longest_line_width =
|
||||||
layout_line(snapshot.longest_row(), &snapshot, &style, cx).width;
|
layout_line(snapshot.longest_row(), &snapshot, &style, editor_width, cx)
|
||||||
|
.width;
|
||||||
let mut scroll_width =
|
let mut scroll_width =
|
||||||
longest_line_width.max(max_visible_line_width) + overscroll.width;
|
longest_line_width.max(max_visible_line_width) + overscroll.width;
|
||||||
|
|
||||||
|
@ -5181,6 +5201,7 @@ impl Element for EditorElement {
|
||||||
&snapshot,
|
&snapshot,
|
||||||
&hitbox,
|
&hitbox,
|
||||||
&text_hitbox,
|
&text_hitbox,
|
||||||
|
editor_width,
|
||||||
&mut scroll_width,
|
&mut scroll_width,
|
||||||
&gutter_dimensions,
|
&gutter_dimensions,
|
||||||
em_width,
|
em_width,
|
||||||
|
@ -5952,10 +5973,20 @@ fn layout_line(
|
||||||
row: DisplayRow,
|
row: DisplayRow,
|
||||||
snapshot: &EditorSnapshot,
|
snapshot: &EditorSnapshot,
|
||||||
style: &EditorStyle,
|
style: &EditorStyle,
|
||||||
|
text_width: Pixels,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> LineWithInvisibles {
|
) -> LineWithInvisibles {
|
||||||
let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style);
|
let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style);
|
||||||
LineWithInvisibles::from_chunks(chunks, &style.text, MAX_LINE_LEN, 1, &[], snapshot.mode, cx)
|
LineWithInvisibles::from_chunks(
|
||||||
|
chunks,
|
||||||
|
&style.text,
|
||||||
|
MAX_LINE_LEN,
|
||||||
|
1,
|
||||||
|
&[],
|
||||||
|
snapshot.mode,
|
||||||
|
text_width,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
.pop()
|
.pop()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ use async_watch as watch;
|
||||||
pub use clock::ReplicaId;
|
pub use clock::ReplicaId;
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, AppContext, EventEmitter, HighlightStyle, ModelContext, Task, TaskLabel,
|
AnyElement, AppContext, EventEmitter, HighlightStyle, ModelContext, Pixels, Task, TaskLabel,
|
||||||
WindowContext,
|
WindowContext,
|
||||||
};
|
};
|
||||||
use lsp::LanguageServerId;
|
use lsp::LanguageServerId;
|
||||||
|
@ -40,7 +40,7 @@ use std::{
|
||||||
future::Future,
|
future::Future,
|
||||||
iter::{self, Iterator, Peekable},
|
iter::{self, Iterator, Peekable},
|
||||||
mem,
|
mem,
|
||||||
ops::{Deref, Range},
|
ops::{Deref, DerefMut, Range},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
str,
|
str,
|
||||||
sync::{Arc, LazyLock},
|
sync::{Arc, LazyLock},
|
||||||
|
@ -486,11 +486,16 @@ pub struct Chunk<'a> {
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ChunkRenderer {
|
pub struct ChunkRenderer {
|
||||||
/// creates a custom element to represent this chunk.
|
/// creates a custom element to represent this chunk.
|
||||||
pub render: Arc<dyn Send + Sync + Fn(&mut WindowContext) -> AnyElement>,
|
pub render: Arc<dyn Send + Sync + Fn(&mut ChunkRendererContext) -> AnyElement>,
|
||||||
/// If true, the element is constrained to the shaped width of the text.
|
/// If true, the element is constrained to the shaped width of the text.
|
||||||
pub constrain_width: bool,
|
pub constrain_width: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ChunkRendererContext<'a, 'b> {
|
||||||
|
pub context: &'a mut WindowContext<'b>,
|
||||||
|
pub max_width: Pixels,
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Debug for ChunkRenderer {
|
impl fmt::Debug for ChunkRenderer {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
f.debug_struct("ChunkRenderer")
|
f.debug_struct("ChunkRenderer")
|
||||||
|
@ -499,6 +504,20 @@ impl fmt::Debug for ChunkRenderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> Deref for ChunkRendererContext<'a, 'b> {
|
||||||
|
type Target = WindowContext<'b>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> DerefMut for ChunkRendererContext<'a, 'b> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
self.context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A set of edits to a given version of a buffer, computed asynchronously.
|
/// A set of edits to a given version of a buffer, computed asynchronously.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Diff {
|
pub struct Diff {
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
(struct_item
|
(struct_item
|
||||||
(visibility_modifier)? @context
|
(visibility_modifier)? @context
|
||||||
"struct" @context
|
"struct" @context
|
||||||
name: (_) @name) @item
|
name: (_) @name
|
||||||
|
body: (_ "{" @open (_)* "}" @close)) @item
|
||||||
|
|
||||||
(enum_item
|
(enum_item
|
||||||
(visibility_modifier)? @context
|
(visibility_modifier)? @context
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue