Restructure assistant edits to show all changes in a proposed-change editor (#18240)
This changes the `/workflow` command so that instead of emitting edits in separate steps, the user is presented with a single tab, with an editable diff that they can apply to the buffer. Todo * Assistant panel * [x] Show a patch title and a list of changed files in a block decoration * [x] Don't store resolved patches as state on Context. Resolve on demand. * [ ] Better presentation of patches in the panel * [ ] Show a spinner while patch is streaming in * Patches * [x] Preserve leading whitespace in new text, auto-indent insertions * [x] Ensure patch title is very short, to fit better in tab * [x] Improve patch location resolution, prefer skipping whitespace over skipping `}` * [x] Ensure patch edits are auto-indented properly * [ ] Apply `Update` edits via a diff between the old and new text, to get fine-grained edits. * Proposed changes editor * [x] Show patch title in the tab * [x] Add a toolbar with an "Apply all" button * [x] Make `open excerpts` open the corresponding location in the base buffer (https://github.com/zed-industries/zed/pull/18591) * [x] Add an apply button above every hunk (https://github.com/zed-industries/zed/pull/18592) * [x] Expand all diff hunks by default (https://github.com/zed-industries/zed/pull/18598) * [x] Fix https://github.com/zed-industries/zed/issues/18589 * [x] Syntax highlighting doesn't work until the buffer is edited (https://github.com/zed-industries/zed/pull/18648) * [x] Disable LSP interaction in Proposed Changes editor (https://github.com/zed-industries/zed/pull/18945) * [x] No auto-indent? (https://github.com/zed-industries/zed/pull/18984) * Prompt * [ ] make sure old_text is unique Release Notes: - N/A --------- Co-authored-by: Marshall Bowers <elliott.codes@gmail.com> Co-authored-by: Antonio <antonio@zed.dev> Co-authored-by: Richard <richard@zed.dev> Co-authored-by: Marshall <marshall@zed.dev> Co-authored-by: Nate Butler <iamnbutler@gmail.com> Co-authored-by: Antonio Scandurra <me@as-cii.com> Co-authored-by: Richard Feldman <oss@rtfeldman.com>
This commit is contained in:
parent
4ae2f93086
commit
411f64b374
22 changed files with 1699 additions and 2816 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -412,6 +412,7 @@ dependencies = [
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"paths",
|
"paths",
|
||||||
"picker",
|
"picker",
|
||||||
|
"pretty_assertions",
|
||||||
"project",
|
"project",
|
||||||
"proto",
|
"proto",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
|
1
assets/icons/diff.svg
Normal file
1
assets/icons/diff.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-diff"><path d="M12 3v14"/><path d="M5 10h14"/><path d="M5 21h14"/></svg>
|
After Width: | Height: | Size: 275 B |
|
@ -1,85 +1,33 @@
|
||||||
<task_description>
|
<task_description>
|
||||||
|
|
||||||
# Code Change Workflow
|
The user of a code editor wants to make a change to their codebase.
|
||||||
|
You must describe the change using the following XML structure:
|
||||||
|
|
||||||
Your task is to guide the user through code changes using a series of steps. Each step should describe a high-level change, which can consist of multiple edits to distinct locations in the codebase.
|
- <patch> - A group of related code changes.
|
||||||
|
Child tags:
|
||||||
## Output Example
|
- <title> (required) - A high-level description of the changes. This should be as short
|
||||||
|
as possible, possibly using common abbreviations.
|
||||||
Provide output as XML, with the following format:
|
- <edit> (1 or more) - An edit to make at a particular range within a file.
|
||||||
|
Includes the following child tags:
|
||||||
<step>
|
- <path> (required) - The path to the file that will be changed.
|
||||||
Update the Person struct to store an age
|
- <description> (optional) - An arbitrarily-long comment that describes the purpose
|
||||||
|
of this edit.
|
||||||
```rust
|
- <old_text> (optional) - An excerpt from the file's current contents that uniquely
|
||||||
struct Person {
|
identifies a range within the file where the edit should occur. If this tag is not
|
||||||
// existing fields...
|
specified, then the entire file will be used as the range.
|
||||||
age: u8,
|
- <new_text> (required) - The new text to insert into the file.
|
||||||
height: f32,
|
- <operation> (required) - The type of change that should occur at the given range
|
||||||
// existing fields...
|
of the file. Must be one of the following values:
|
||||||
}
|
- `update`: Replaces the entire range with the new text.
|
||||||
|
- `insert_before`: Inserts the new text before the range.
|
||||||
impl Person {
|
- `insert_after`: Inserts new text after the range.
|
||||||
fn age(&self) -> u8 {
|
- `create`: Creates a new file with the given path and the new text.
|
||||||
self.age
|
- `delete`: Deletes the specified range from the file.
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
<edit>
|
|
||||||
<path>src/person.rs</path>
|
|
||||||
<operation>insert_before</operation>
|
|
||||||
<search>height: f32,</search>
|
|
||||||
<description>Add the age field</description>
|
|
||||||
</edit>
|
|
||||||
|
|
||||||
<edit>
|
|
||||||
<path>src/person.rs</path>
|
|
||||||
<operation>insert_after</operation>
|
|
||||||
<search>impl Person {</search>
|
|
||||||
<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.
|
|
||||||
|
|
||||||
After the description, each `<step>` must contain one or more `<edit>` tags, each of which refer to a specific range in a source file. Each `<edit>` tag must contain the following child tags:
|
|
||||||
|
|
||||||
### `<path>` (required)
|
|
||||||
|
|
||||||
This tag contains the path to the file that will be changed. It can be an existing path, or a path that should be created.
|
|
||||||
|
|
||||||
### `<search>` (optional)
|
|
||||||
|
|
||||||
This tag contains a search string to locate in the source file, e.g. `pub fn baz() {`. If not provided, the new content will be inserted at the top of the file. Make sure to produce a string that exists in the source file and that isn't ambiguous. When there's ambiguity, add more lines to the search to eliminate it.
|
|
||||||
|
|
||||||
### `<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 string entirely based on the given description.
|
|
||||||
- `create`: Creates a new file with the given path based on the provided description.
|
|
||||||
- `insert_before`: Inserts new text based on the given description before the specified search string.
|
|
||||||
- `insert_after`: Inserts new text based on the given description after the specified search string.
|
|
||||||
- `delete`: Deletes the specified string from the containing file.
|
|
||||||
|
|
||||||
<guidelines>
|
<guidelines>
|
||||||
- There's no need to describe *what* to do, just *where* to do it.
|
- Never provide multiple edits whose ranges intersect each other. Instead, merge them into one edit.
|
||||||
- Only reference locations that actually exist (unless you're creating a file).
|
- Prefer multiple edits to smaller, disjoint ranges, rather than one edit to a larger range.
|
||||||
- If creating a file, assume any subsequent updates are included at the time of creation.
|
- There's no need to escape angle brackets within XML tags.
|
||||||
- Don't create and then update a file. Always create new files in one hot.
|
|
||||||
- Prefer multiple edits to smaller regions, as opposed to one big edit to a larger region.
|
|
||||||
- Don't produce edits that intersect each other. In that case, merge them into a bigger edit.
|
|
||||||
- Never nest an edit with another edit. Never include CDATA. All edits are leaf nodes.
|
|
||||||
- Descriptions are required for all edits except delete.
|
|
||||||
- When generating multiple edits, ensure the descriptions are specific to each individual operation.
|
|
||||||
- Avoid referring to the search string in the description. Focus on the change to be made, not the location where it's made. That's implicit with the `search` string you provide.
|
|
||||||
- Don't generate multiple edits at the same location. Instead, combine them together in a single edit with a succinct combined description.
|
|
||||||
- Always ensure imports are added if you're referencing symbols that are not in scope.
|
- Always ensure imports are added if you're referencing symbols that are not in scope.
|
||||||
</guidelines>
|
</guidelines>
|
||||||
|
|
||||||
|
@ -124,189 +72,137 @@ Update all shapes to store their origin as an (x, y) tuple and implement Display
|
||||||
<message role="assistant">
|
<message role="assistant">
|
||||||
We'll need to update both the rectangle and circle modules.
|
We'll need to update both the rectangle and circle modules.
|
||||||
|
|
||||||
<step>
|
<patch>
|
||||||
Add origin fields to both shape types.
|
<title>Add origins and display impls to shapes</title>
|
||||||
|
<edit>
|
||||||
```rust
|
<path>src/shapes/rectangle.rs</path>
|
||||||
struct Rectangle {
|
<description>Add the origin field to Rectangle struct</description>
|
||||||
// existing fields ...
|
<operation>insert_after</operation>
|
||||||
origin: (f64, f64),
|
<old_text>
|
||||||
}
|
pub struct Rectangle {
|
||||||
```
|
</old_text>
|
||||||
|
<new_text>
|
||||||
```rust
|
origin: (f64, f64),
|
||||||
struct Circle {
|
</new_text>
|
||||||
// existing fields ...
|
</edit>
|
||||||
origin: (f64, f64),
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
<edit>
|
<edit>
|
||||||
<path>src/shapes/rectangle.rs</path>
|
<path>src/shapes/rectangle.rs</path>
|
||||||
<operation>insert_before</operation>
|
<description>Update the Rectangle's new function to take an origin parameter</description>
|
||||||
<search>
|
<operation>update</operation>
|
||||||
width: f64,
|
<old_text>
|
||||||
height: f64,
|
fn new(width: f64, height: f64) -> Self {
|
||||||
</search>
|
Rectangle { width, height }
|
||||||
<description>Add the origin field to Rectangle</description>
|
}
|
||||||
|
</old_text>
|
||||||
|
<new_text>
|
||||||
|
fn new(origin: (f64, f64), width: f64, height: f64) -> Self {
|
||||||
|
Rectangle { origin, width, height }
|
||||||
|
}
|
||||||
|
</new_text>
|
||||||
</edit>
|
</edit>
|
||||||
|
|
||||||
<edit>
|
<edit>
|
||||||
<path>src/shapes/circle.rs</path>
|
<path>src/shapes/circle.rs</path>
|
||||||
<operation>insert_before</operation>
|
<description>Add the origin field to Circle struct</description>
|
||||||
<search>
|
<operation>insert_after</operation>
|
||||||
|
<old_text>
|
||||||
|
pub struct Circle {
|
||||||
radius: f64,
|
radius: f64,
|
||||||
</search>
|
</old_text>
|
||||||
<description>Add the origin field to Circle</description>
|
<new_text>
|
||||||
</edit>
|
origin: (f64, f64),
|
||||||
|
</new_text>
|
||||||
<step>
|
|
||||||
Update both shape's constructors to take an origin.
|
|
||||||
|
|
||||||
<edit>
|
|
||||||
<path>src/shapes/rectangle.rs</path>
|
|
||||||
<operation>update</operation>
|
|
||||||
<search>
|
|
||||||
fn new(width: f64, height: f64) -> Self {
|
|
||||||
Rectangle { width, height }
|
|
||||||
}
|
|
||||||
</search>
|
|
||||||
<description>Update the Rectangle new function to take an origin</description>
|
|
||||||
</edit>
|
</edit>
|
||||||
|
|
||||||
<edit>
|
<edit>
|
||||||
<path>src/shapes/circle.rs</path>
|
<path>src/shapes/circle.rs</path>
|
||||||
|
<description>Update the Circle's new function to take an origin parameter</description>
|
||||||
<operation>update</operation>
|
<operation>update</operation>
|
||||||
<search>
|
<old_text>
|
||||||
fn new(radius: f64) -> Self {
|
fn new(radius: f64) -> Self {
|
||||||
Circle { radius }
|
Circle { radius }
|
||||||
}
|
}
|
||||||
</search>
|
</old_text>
|
||||||
<description>Update the Circle new function to take an origin</description>
|
<new_text>
|
||||||
|
fn new(origin: (f64, f64), radius: f64) -> Self {
|
||||||
|
Circle { origin, radius }
|
||||||
|
}
|
||||||
|
</new_text>
|
||||||
</edit>
|
</edit>
|
||||||
</step>
|
</step>
|
||||||
|
|
||||||
<step>
|
|
||||||
Implement Display for both shapes
|
|
||||||
|
|
||||||
<edit>
|
<edit>
|
||||||
<path>src/shapes/rectangle.rs</path>
|
<path>src/shapes/rectangle.rs</path>
|
||||||
|
<description>Add an import for the std::fmt module</description>
|
||||||
<operation>insert_before</operation>
|
<operation>insert_before</operation>
|
||||||
<search>
|
<old_text>
|
||||||
struct Rectangle {
|
struct Rectangle {
|
||||||
</search>
|
</old_text>
|
||||||
<description>Add an import for the `std::fmt` module</description>
|
<new_text>
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
</new_text>
|
||||||
</edit>
|
</edit>
|
||||||
|
|
||||||
<edit>
|
<edit>
|
||||||
<path>src/shapes/rectangle.rs</path>
|
<path>src/shapes/rectangle.rs</path>
|
||||||
|
<description>Add a Display implementation for Rectangle</description>
|
||||||
<operation>insert_after</operation>
|
<operation>insert_after</operation>
|
||||||
<search>
|
<old_text>
|
||||||
Rectangle { width, height }
|
Rectangle { width, height }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</search>
|
</old_text>
|
||||||
<description>Add a Display implementation for Rectangle</description>
|
<new_text>
|
||||||
|
impl fmt::Display for Rectangle {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.format_struct(f, "Rectangle")
|
||||||
|
.field("origin", &self.origin)
|
||||||
|
.field("width", &self.width)
|
||||||
|
.field("height", &self.height)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</new_text>
|
||||||
</edit>
|
</edit>
|
||||||
|
|
||||||
<edit>
|
<edit>
|
||||||
<path>src/shapes/circle.rs</path>
|
<path>src/shapes/circle.rs</path>
|
||||||
<operation>insert_before</operation>
|
|
||||||
<search>
|
|
||||||
struct Circle {
|
|
||||||
</search>
|
|
||||||
<description>Add an import for the `std::fmt` module</description>
|
<description>Add an import for the `std::fmt` module</description>
|
||||||
|
<operation>insert_before</operation>
|
||||||
|
<old_text>
|
||||||
|
struct Circle {
|
||||||
|
</old_text>
|
||||||
|
<new_text>
|
||||||
|
use std::fmt;
|
||||||
|
</new_text>
|
||||||
</edit>
|
</edit>
|
||||||
|
|
||||||
<edit>
|
<edit>
|
||||||
<path>src/shapes/circle.rs</path>
|
<path>src/shapes/circle.rs</path>
|
||||||
|
<description>Add a Display implementation for Circle</description>
|
||||||
<operation>insert_after</operation>
|
<operation>insert_after</operation>
|
||||||
<search>
|
<old_text>
|
||||||
Circle { radius }
|
Circle { radius }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</search>
|
</old_text>
|
||||||
<description>Add a Display implementation for Circle</description>
|
<new_text>
|
||||||
|
impl fmt::Display for Rectangle {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.format_struct(f, "Rectangle")
|
||||||
|
.field("origin", &self.origin)
|
||||||
|
.field("width", &self.width)
|
||||||
|
.field("height", &self.height)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</new_text>
|
||||||
</edit>
|
</edit>
|
||||||
</step>
|
</patch>
|
||||||
|
|
||||||
</message>
|
</message>
|
||||||
</example>
|
</example>
|
||||||
|
|
||||||
<example number=2>
|
|
||||||
<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
|
|
||||||
impl User {
|
|
||||||
// ... other methods ...
|
|
||||||
|
|
||||||
pub fn print_info(&self) {
|
|
||||||
println!("Name: {name}, Age: {age}", name = self.name, age = self.age);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
<edit>
|
|
||||||
<path>src/user.rs</path>
|
|
||||||
<operation>update</operation>
|
|
||||||
<search>
|
|
||||||
pub fn print_info(&self) {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
</search>
|
|
||||||
<description>Print all the user information</description>
|
|
||||||
</edit>
|
|
||||||
</step>
|
|
||||||
|
|
||||||
<step>
|
|
||||||
Remove the 'email' field from the User struct
|
|
||||||
|
|
||||||
<edit>
|
|
||||||
<path>src/user.rs</path>
|
|
||||||
<operation>delete</operation>
|
|
||||||
<search>
|
|
||||||
email: String,
|
|
||||||
</search>
|
|
||||||
</edit>
|
|
||||||
|
|
||||||
<edit>
|
|
||||||
<path>src/user.rs</path>
|
|
||||||
<operation>update</operation>
|
|
||||||
<symbol>
|
|
||||||
fn new(name: String, age: u32, email: String) -> Self {
|
|
||||||
User { name, age, email }
|
|
||||||
}
|
|
||||||
</symbol>
|
|
||||||
<description>Remove email parameter from new method</description>
|
|
||||||
</edit>
|
|
||||||
</step>
|
|
||||||
</message>
|
|
||||||
</example>
|
|
||||||
|
|
||||||
You should think step by step. When possible, produce smaller, coherent logical steps as opposed to one big step that combines lots of heterogeneous edits.
|
|
||||||
|
|
||||||
</task_description>
|
</task_description>
|
||||||
|
|
|
@ -1,496 +0,0 @@
|
||||||
<overview>
|
|
||||||
Your task is to map a step from a workflow to locations in source code where code needs to be changed to fulfill that step.
|
|
||||||
Given a workflow containing background context plus a series of <step> tags, you will resolve *one* of these step tags to resolve to one or more locations in the code.
|
|
||||||
With each location, you will produce a brief, one-line description of the changes to be made.
|
|
||||||
|
|
||||||
<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 suggestions on a parent symbol and one of its children in the same suggestions block.
|
|
||||||
- Never nest an operation with another operation or include CDATA or other content. All suggestions are leaf nodes.
|
|
||||||
- Descriptions are required for all suggestions except delete.
|
|
||||||
- When generating multiple suggestions, ensure the descriptions are specific to each individual operation.
|
|
||||||
- Avoid referring to the location in the description. Focus on the change to be made, not the location where it's made. That's implicit with the symbol you provide.
|
|
||||||
- Don't generate multiple suggestions at the same location. Instead, combine them together in a single operation with a succinct combined description.
|
|
||||||
- To add imports respond with a suggestion where the `"symbol"` key is set to `"#imports"`
|
|
||||||
</guidelines>
|
|
||||||
</overview>
|
|
||||||
|
|
||||||
<examples>
|
|
||||||
<example>
|
|
||||||
<workflow_context>
|
|
||||||
<message role="user">
|
|
||||||
```rs src/rectangle.rs
|
|
||||||
struct Rectangle {
|
|
||||||
width: f64,
|
|
||||||
height: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Rectangle {
|
|
||||||
fn new(width: f64, height: f64) -> Self {
|
|
||||||
Rectangle { width, height }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
We need to add methods to calculate the area and perimeter of the rectangle. Can you help with that?
|
|
||||||
</message>
|
|
||||||
<message role="assistant">
|
|
||||||
Sure, I can help with that!
|
|
||||||
|
|
||||||
<step>Add new methods 'calculate_area' and 'calculate_perimeter' to the Rectangle struct</step>
|
|
||||||
<step>Implement the 'Display' trait for the Rectangle struct</step>
|
|
||||||
</message>
|
|
||||||
</workflow_context>
|
|
||||||
|
|
||||||
<step_to_resolve>
|
|
||||||
Add new methods 'calculate_area' and 'calculate_perimeter' to the Rectangle struct
|
|
||||||
</step_to_resolve>
|
|
||||||
|
|
||||||
<incorrect_output reason="NEVER append multiple children at the same location.">
|
|
||||||
{
|
|
||||||
"title": "Add Rectangle methods",
|
|
||||||
"suggestions": [
|
|
||||||
{
|
|
||||||
"kind": "AppendChild",
|
|
||||||
"path": "src/shapes.rs",
|
|
||||||
"symbol": "impl Rectangle",
|
|
||||||
"description": "Add calculate_area method"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "AppendChild",
|
|
||||||
"path": "src/shapes.rs",
|
|
||||||
"symbol": "impl Rectangle",
|
|
||||||
"description": "Add calculate_perimeter method"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</incorrect_output>
|
|
||||||
|
|
||||||
<correct_output>
|
|
||||||
{
|
|
||||||
"title": "Add Rectangle methods",
|
|
||||||
"suggestions": [
|
|
||||||
{
|
|
||||||
"kind": "AppendChild",
|
|
||||||
"path": "src/shapes.rs",
|
|
||||||
"symbol": "impl Rectangle",
|
|
||||||
"description": "Add calculate area and perimeter methods"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</correct_output>
|
|
||||||
|
|
||||||
<step_to_resolve>
|
|
||||||
Implement the 'Display' trait for the Rectangle struct
|
|
||||||
</step_to_resolve>
|
|
||||||
|
|
||||||
<output>
|
|
||||||
{
|
|
||||||
"title": "Implement Display for Rectangle",
|
|
||||||
"suggestions": [
|
|
||||||
{
|
|
||||||
"kind": "InsertSiblingAfter",
|
|
||||||
"path": "src/shapes.rs",
|
|
||||||
"symbol": "impl Rectangle",
|
|
||||||
"description": "Implement Display trait for Rectangle"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</output>
|
|
||||||
|
|
||||||
<example>
|
|
||||||
<workflow_context>
|
|
||||||
<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) {
|
|
||||||
println!("Name: {}, Age: {}, Email: {}", self.name, self.age, self.email);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</message>
|
|
||||||
<message role="assistant">
|
|
||||||
Certainly!
|
|
||||||
<step>Update the 'print_info' method to use formatted output</step>
|
|
||||||
<step>Remove the 'email' field from the User struct</step>
|
|
||||||
</message>
|
|
||||||
</workflow_context>
|
|
||||||
|
|
||||||
<step_to_resolve>
|
|
||||||
Update the 'print_info' method to use formatted output
|
|
||||||
</step_to_resolve>
|
|
||||||
|
|
||||||
<output>
|
|
||||||
{
|
|
||||||
"title": "Use formatted output",
|
|
||||||
"suggestions": [
|
|
||||||
{
|
|
||||||
"kind": "Update",
|
|
||||||
"path": "src/user.rs",
|
|
||||||
"symbol": "impl User pub fn print_info",
|
|
||||||
"description": "Use formatted output"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</output>
|
|
||||||
|
|
||||||
<step_to_resolve>
|
|
||||||
Remove the 'email' field from the User struct
|
|
||||||
</step_to_resolve>
|
|
||||||
|
|
||||||
<output>
|
|
||||||
{
|
|
||||||
"title": "Remove email field",
|
|
||||||
"suggestions": [
|
|
||||||
{
|
|
||||||
"kind": "Delete",
|
|
||||||
"path": "src/user.rs",
|
|
||||||
"symbol": "struct User email"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</output>
|
|
||||||
</example>
|
|
||||||
|
|
||||||
<example>
|
|
||||||
<workflow_context>
|
|
||||||
<message role="user">
|
|
||||||
```rs src/vehicle.rs
|
|
||||||
struct Vehicle {
|
|
||||||
make: String,
|
|
||||||
model: String,
|
|
||||||
year: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Vehicle {
|
|
||||||
fn new(make: String, model: String, year: u32) -> Self {
|
|
||||||
Vehicle { make, model, year }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_year(&self) {
|
|
||||||
println!("Year: {}", self.year);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</message>
|
|
||||||
<message role="assistant">
|
|
||||||
<step>Add a 'use std::fmt;' statement at the beginning of the file</step>
|
|
||||||
<step>Add a new method 'start_engine' in the Vehicle impl block</step>
|
|
||||||
</message>
|
|
||||||
</workflow_context>
|
|
||||||
|
|
||||||
<step_to_resolve>
|
|
||||||
Add a 'use std::fmt;' statement at the beginning of the file
|
|
||||||
</step_to_resolve>
|
|
||||||
|
|
||||||
<output>
|
|
||||||
{
|
|
||||||
"title": "Add use std::fmt statement",
|
|
||||||
"suggestions": [
|
|
||||||
{
|
|
||||||
"kind": "PrependChild",
|
|
||||||
"path": "src/vehicle.rs",
|
|
||||||
"symbol": "#imports",
|
|
||||||
"description": "Add 'use std::fmt' statement"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</output>
|
|
||||||
|
|
||||||
<step_to_resolve>
|
|
||||||
Add a new method 'start_engine' in the Vehicle impl block
|
|
||||||
</step_to_resolve>
|
|
||||||
|
|
||||||
<output>
|
|
||||||
{
|
|
||||||
"title": "Add start_engine method",
|
|
||||||
"suggestions": [
|
|
||||||
{
|
|
||||||
"kind": "InsertSiblingAfter",
|
|
||||||
"path": "src/vehicle.rs",
|
|
||||||
"symbol": "impl Vehicle fn new",
|
|
||||||
"description": "Add start_engine method"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</output>
|
|
||||||
</example>
|
|
||||||
|
|
||||||
<example>
|
|
||||||
<workflow_context>
|
|
||||||
<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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</message>
|
|
||||||
<message role="assistant">
|
|
||||||
<step>Make salary an f32</step>
|
|
||||||
<step>Remove the 'department' field and update the 'print_details' method</step>
|
|
||||||
</message>
|
|
||||||
</workflow_context>
|
|
||||||
|
|
||||||
<step_to_resolve>
|
|
||||||
Make salary an f32
|
|
||||||
</step_to_resolve>
|
|
||||||
|
|
||||||
<incorrect_output reason="NEVER include suggestions on a parent symbol and one of its children in the same suggestions block.">
|
|
||||||
{
|
|
||||||
"title": "Change salary to f32",
|
|
||||||
"suggestions": [
|
|
||||||
{
|
|
||||||
"kind": "Update",
|
|
||||||
"path": "src/employee.rs",
|
|
||||||
"symbol": "struct Employee",
|
|
||||||
"description": "Change the type of salary to an f32"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Update",
|
|
||||||
"path": "src/employee.rs",
|
|
||||||
"symbol": "struct Employee salary",
|
|
||||||
"description": "Change the type to an f32"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</incorrect_output>
|
|
||||||
|
|
||||||
<correct_output>
|
|
||||||
{
|
|
||||||
"title": "Change salary to f32",
|
|
||||||
"suggestions": [
|
|
||||||
{
|
|
||||||
"kind": "Update",
|
|
||||||
"path": "src/employee.rs",
|
|
||||||
"symbol": "struct Employee salary",
|
|
||||||
"description": "Change the type to an f32"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</correct_output>
|
|
||||||
|
|
||||||
<step_to_resolve>
|
|
||||||
Remove the 'department' field and update the 'print_details' method
|
|
||||||
</step_to_resolve>
|
|
||||||
|
|
||||||
<output>
|
|
||||||
{
|
|
||||||
"title": "Remove department",
|
|
||||||
"suggestions": [
|
|
||||||
{
|
|
||||||
"kind": "Delete",
|
|
||||||
"path": "src/employee.rs",
|
|
||||||
"symbol": "struct Employee department"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Update",
|
|
||||||
"path": "src/employee.rs",
|
|
||||||
"symbol": "impl Employee fn print_details",
|
|
||||||
"description": "Don't print the 'department' field"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</output>
|
|
||||||
</example>
|
|
||||||
|
|
||||||
<example>
|
|
||||||
<workflow_context>
|
|
||||||
<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() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</message>
|
|
||||||
<message role="assistant">
|
|
||||||
<step>Add a 'level' field to Player and update the 'new' method</step>
|
|
||||||
</message>
|
|
||||||
</workflow_context>
|
|
||||||
|
|
||||||
<step_to_resolve>
|
|
||||||
Add a 'level' field to Player and update the 'new' method
|
|
||||||
</step_to_resolve>
|
|
||||||
|
|
||||||
<output>
|
|
||||||
{
|
|
||||||
"title": "Add level field to Player",
|
|
||||||
"suggestions": [
|
|
||||||
{
|
|
||||||
"kind": "InsertSiblingAfter",
|
|
||||||
"path": "src/game.rs",
|
|
||||||
"symbol": "struct Player pub score",
|
|
||||||
"description": "Add level field to Player"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Update",
|
|
||||||
"path": "src/game.rs",
|
|
||||||
"symbol": "impl Player pub fn new",
|
|
||||||
"description": "Initialize level in new method"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</output>
|
|
||||||
</example>
|
|
||||||
|
|
||||||
<example>
|
|
||||||
<workflow_context>
|
|
||||||
<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() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</message>
|
|
||||||
<message role="assistant">
|
|
||||||
<step>Add a 'load_from_file' method to Config and import necessary modules</step>
|
|
||||||
</message>
|
|
||||||
</workflow_context>
|
|
||||||
|
|
||||||
<step_to_resolve>
|
|
||||||
Add a 'load_from_file' method to Config and import necessary modules
|
|
||||||
</step_to_resolve>
|
|
||||||
|
|
||||||
<output>
|
|
||||||
{
|
|
||||||
"title": "Add load_from_file method",
|
|
||||||
"suggestions": [
|
|
||||||
{
|
|
||||||
"kind": "PrependChild",
|
|
||||||
"path": "src/config.rs",
|
|
||||||
"symbol": "#imports",
|
|
||||||
"description": "Import std::fs and std::io modules"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "AppendChild",
|
|
||||||
"path": "src/config.rs",
|
|
||||||
"symbol": "impl Config",
|
|
||||||
"description": "Add load_from_file method"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</output>
|
|
||||||
</example>
|
|
||||||
|
|
||||||
<example>
|
|
||||||
<workflow_context>
|
|
||||||
<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, &[])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</message>
|
|
||||||
<message role="assistant">
|
|
||||||
<step>Add error handling to the 'query' method and create a custom error type</step>
|
|
||||||
</message>
|
|
||||||
</workflow_context>
|
|
||||||
|
|
||||||
<step_to_resolve>
|
|
||||||
Add error handling to the 'query' method and create a custom error type
|
|
||||||
</step_to_resolve>
|
|
||||||
|
|
||||||
<output>
|
|
||||||
{
|
|
||||||
"title": "Add error handling to query",
|
|
||||||
"suggestions": [
|
|
||||||
{
|
|
||||||
"kind": "PrependChild",
|
|
||||||
"path": "src/database.rs",
|
|
||||||
"description": "Import necessary error handling modules"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "InsertSiblingBefore",
|
|
||||||
"path": "src/database.rs",
|
|
||||||
"symbol": "pub(crate) struct Database",
|
|
||||||
"description": "Define custom DatabaseError enum"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Update",
|
|
||||||
"path": "src/database.rs",
|
|
||||||
"symbol": "impl Database async fn query",
|
|
||||||
"description": "Implement error handling in query method"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</output>
|
|
||||||
</example>
|
|
||||||
</examples>
|
|
||||||
|
|
||||||
Now generate the suggestions for the following step:
|
|
||||||
|
|
||||||
<workflow_context>
|
|
||||||
{{{workflow_context}}}
|
|
||||||
</workflow_context>
|
|
||||||
|
|
||||||
<step_to_resolve>
|
|
||||||
{{{step_to_resolve}}}
|
|
||||||
</step_to_resolve>
|
|
|
@ -97,6 +97,7 @@ language = { workspace = true, features = ["test-support"] }
|
||||||
language_model = { workspace = true, features = ["test-support"] }
|
language_model = { workspace = true, features = ["test-support"] }
|
||||||
languages = { workspace = true, features = ["test-support"] }
|
languages = { workspace = true, features = ["test-support"] }
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
pretty_assertions.workspace = true
|
||||||
project = { workspace = true, features = ["test-support"] }
|
project = { workspace = true, features = ["test-support"] }
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
serde_json_lenient.workspace = true
|
serde_json_lenient.workspace = true
|
||||||
|
|
|
@ -6,6 +6,7 @@ mod context;
|
||||||
pub mod context_store;
|
pub mod context_store;
|
||||||
mod inline_assistant;
|
mod inline_assistant;
|
||||||
mod model_selector;
|
mod model_selector;
|
||||||
|
mod patch;
|
||||||
mod prompt_library;
|
mod prompt_library;
|
||||||
mod prompts;
|
mod prompts;
|
||||||
mod slash_command;
|
mod slash_command;
|
||||||
|
@ -14,7 +15,6 @@ pub mod slash_command_settings;
|
||||||
mod streaming_diff;
|
mod streaming_diff;
|
||||||
mod terminal_inline_assistant;
|
mod terminal_inline_assistant;
|
||||||
mod tools;
|
mod tools;
|
||||||
mod workflow;
|
|
||||||
|
|
||||||
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
|
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
|
||||||
use assistant_settings::AssistantSettings;
|
use assistant_settings::AssistantSettings;
|
||||||
|
@ -35,11 +35,13 @@ use language_model::{
|
||||||
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
|
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
|
||||||
};
|
};
|
||||||
pub(crate) use model_selector::*;
|
pub(crate) use model_selector::*;
|
||||||
|
pub use patch::*;
|
||||||
pub use prompts::PromptBuilder;
|
pub use prompts::PromptBuilder;
|
||||||
use prompts::PromptLoadingParams;
|
use prompts::PromptLoadingParams;
|
||||||
use semantic_index::{CloudEmbeddingProvider, SemanticDb};
|
use semantic_index::{CloudEmbeddingProvider, SemanticDb};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{update_settings_file, Settings, SettingsStore};
|
use settings::{update_settings_file, Settings, SettingsStore};
|
||||||
|
use slash_command::workflow_command::WorkflowSlashCommand;
|
||||||
use slash_command::{
|
use slash_command::{
|
||||||
auto_command, cargo_workspace_command, context_server_command, default_command, delta_command,
|
auto_command, cargo_workspace_command, context_server_command, default_command, delta_command,
|
||||||
diagnostics_command, docs_command, fetch_command, file_command, now_command, project_command,
|
diagnostics_command, docs_command, fetch_command, file_command, now_command, project_command,
|
||||||
|
@ -50,7 +52,6 @@ use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
pub(crate) use streaming_diff::*;
|
pub(crate) use streaming_diff::*;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
pub use workflow::*;
|
|
||||||
|
|
||||||
use crate::slash_command_settings::SlashCommandSettings;
|
use crate::slash_command_settings::SlashCommandSettings;
|
||||||
|
|
||||||
|
@ -393,12 +394,25 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
|
||||||
slash_command_registry.register_command(now_command::NowSlashCommand, false);
|
slash_command_registry.register_command(now_command::NowSlashCommand, false);
|
||||||
slash_command_registry.register_command(diagnostics_command::DiagnosticsSlashCommand, true);
|
slash_command_registry.register_command(diagnostics_command::DiagnosticsSlashCommand, true);
|
||||||
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
|
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
|
||||||
|
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
|
||||||
|
|
||||||
if let Some(prompt_builder) = prompt_builder {
|
if let Some(prompt_builder) = prompt_builder {
|
||||||
slash_command_registry.register_command(
|
cx.observe_global::<SettingsStore>({
|
||||||
workflow_command::WorkflowSlashCommand::new(prompt_builder.clone()),
|
let slash_command_registry = slash_command_registry.clone();
|
||||||
true,
|
let prompt_builder = prompt_builder.clone();
|
||||||
);
|
move |cx| {
|
||||||
|
if AssistantSettings::get_global(cx).are_live_diffs_enabled(cx) {
|
||||||
|
slash_command_registry.register_command(
|
||||||
|
workflow_command::WorkflowSlashCommand::new(prompt_builder.clone()),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
slash_command_registry.unregister_command_by_name(WorkflowSlashCommand::NAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
cx.observe_flag::<project_command::ProjectSlashCommandFeatureFlag, _>({
|
cx.observe_flag::<project_command::ProjectSlashCommandFeatureFlag, _>({
|
||||||
let slash_command_registry = slash_command_registry.clone();
|
let slash_command_registry = slash_command_registry.clone();
|
||||||
move |is_enabled, _cx| {
|
move |is_enabled, _cx| {
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -2,6 +2,7 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use ::open_ai::Model as OpenAiModel;
|
use ::open_ai::Model as OpenAiModel;
|
||||||
use anthropic::Model as AnthropicModel;
|
use anthropic::Model as AnthropicModel;
|
||||||
|
use feature_flags::FeatureFlagAppExt;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{AppContext, Pixels};
|
use gpui::{AppContext, Pixels};
|
||||||
use language_model::provider::open_ai;
|
use language_model::provider::open_ai;
|
||||||
|
@ -61,6 +62,13 @@ pub struct AssistantSettings {
|
||||||
pub default_model: LanguageModelSelection,
|
pub default_model: LanguageModelSelection,
|
||||||
pub inline_alternatives: Vec<LanguageModelSelection>,
|
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||||
pub using_outdated_settings_version: bool,
|
pub using_outdated_settings_version: bool,
|
||||||
|
pub enable_experimental_live_diffs: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssistantSettings {
|
||||||
|
pub fn are_live_diffs_enabled(&self, cx: &AppContext) -> bool {
|
||||||
|
cx.is_staff() || self.enable_experimental_live_diffs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Assistant panel settings
|
/// Assistant panel settings
|
||||||
|
@ -238,6 +246,7 @@ impl AssistantSettingsContent {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
inline_alternatives: None,
|
inline_alternatives: None,
|
||||||
|
enable_experimental_live_diffs: None,
|
||||||
},
|
},
|
||||||
VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
|
VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
|
||||||
},
|
},
|
||||||
|
@ -257,6 +266,7 @@ impl AssistantSettingsContent {
|
||||||
.to_string(),
|
.to_string(),
|
||||||
}),
|
}),
|
||||||
inline_alternatives: None,
|
inline_alternatives: None,
|
||||||
|
enable_experimental_live_diffs: None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -373,6 +383,7 @@ impl Default for VersionedAssistantSettingsContent {
|
||||||
default_height: None,
|
default_height: None,
|
||||||
default_model: None,
|
default_model: None,
|
||||||
inline_alternatives: None,
|
inline_alternatives: None,
|
||||||
|
enable_experimental_live_diffs: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -403,6 +414,10 @@ pub struct AssistantSettingsContentV2 {
|
||||||
default_model: Option<LanguageModelSelection>,
|
default_model: Option<LanguageModelSelection>,
|
||||||
/// Additional models with which to generate alternatives when performing inline assists.
|
/// Additional models with which to generate alternatives when performing inline assists.
|
||||||
inline_alternatives: Option<Vec<LanguageModelSelection>>,
|
inline_alternatives: Option<Vec<LanguageModelSelection>>,
|
||||||
|
/// Enable experimental live diffs in the assistant panel.
|
||||||
|
///
|
||||||
|
/// Default: false
|
||||||
|
enable_experimental_live_diffs: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||||
|
@ -525,7 +540,10 @@ impl Settings for AssistantSettings {
|
||||||
);
|
);
|
||||||
merge(&mut settings.default_model, value.default_model);
|
merge(&mut settings.default_model, value.default_model);
|
||||||
merge(&mut settings.inline_alternatives, value.inline_alternatives);
|
merge(&mut settings.inline_alternatives, value.inline_alternatives);
|
||||||
// merge(&mut settings.infer_context, value.infer_context); TODO re-enable this once we ship context inference
|
merge(
|
||||||
|
&mut settings.enable_experimental_live_diffs,
|
||||||
|
value.enable_experimental_live_diffs,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(settings)
|
Ok(settings)
|
||||||
|
@ -584,6 +602,7 @@ mod tests {
|
||||||
dock: None,
|
dock: None,
|
||||||
default_width: None,
|
default_width: None,
|
||||||
default_height: None,
|
default_height: None,
|
||||||
|
enable_experimental_live_diffs: None,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
mod context_tests;
|
mod context_tests;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
prompts::PromptBuilder, slash_command::SlashCommandLine, MessageId, MessageStatus,
|
prompts::PromptBuilder, slash_command::SlashCommandLine, AssistantEdit, AssistantPatch,
|
||||||
WorkflowStep, WorkflowStepEdit, WorkflowStepResolution, WorkflowSuggestionGroup,
|
AssistantPatchStatus, MessageId, MessageStatus,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
use assistant_slash_command::{
|
use assistant_slash_command::{
|
||||||
|
@ -15,13 +15,10 @@ use clock::ReplicaId;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use feature_flags::{FeatureFlag, FeatureFlagAppExt};
|
use feature_flags::{FeatureFlag, FeatureFlagAppExt};
|
||||||
use fs::{Fs, RemoveOptions};
|
use fs::{Fs, RemoveOptions};
|
||||||
use futures::{
|
use futures::{future::Shared, FutureExt, StreamExt};
|
||||||
future::{self, Shared},
|
|
||||||
FutureExt, StreamExt,
|
|
||||||
};
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AppContext, AsyncAppContext, Context as _, EventEmitter, Model, ModelContext, RenderImage,
|
AppContext, Context as _, EventEmitter, Model, ModelContext, RenderImage, SharedString,
|
||||||
SharedString, Subscription, Task,
|
Subscription, Task,
|
||||||
};
|
};
|
||||||
|
|
||||||
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
|
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
|
||||||
|
@ -38,7 +35,7 @@ use project::Project;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{
|
use std::{
|
||||||
cmp::{self, max, Ordering},
|
cmp::{max, Ordering},
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
iter, mem,
|
iter, mem,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
|
@ -300,7 +297,7 @@ pub enum ContextEvent {
|
||||||
MessagesEdited,
|
MessagesEdited,
|
||||||
SummaryChanged,
|
SummaryChanged,
|
||||||
StreamedCompletion,
|
StreamedCompletion,
|
||||||
WorkflowStepsUpdated {
|
PatchesUpdated {
|
||||||
removed: Vec<Range<language::Anchor>>,
|
removed: Vec<Range<language::Anchor>>,
|
||||||
updated: Vec<Range<language::Anchor>>,
|
updated: Vec<Range<language::Anchor>>,
|
||||||
},
|
},
|
||||||
|
@ -454,13 +451,14 @@ pub struct XmlTag {
|
||||||
#[derive(Copy, Clone, Debug, strum::EnumString, PartialEq, Eq, strum::AsRefStr)]
|
#[derive(Copy, Clone, Debug, strum::EnumString, PartialEq, Eq, strum::AsRefStr)]
|
||||||
#[strum(serialize_all = "snake_case")]
|
#[strum(serialize_all = "snake_case")]
|
||||||
pub enum XmlTagKind {
|
pub enum XmlTagKind {
|
||||||
Step,
|
Patch,
|
||||||
|
Title,
|
||||||
Edit,
|
Edit,
|
||||||
Path,
|
Path,
|
||||||
Search,
|
|
||||||
Within,
|
|
||||||
Operation,
|
|
||||||
Description,
|
Description,
|
||||||
|
OldText,
|
||||||
|
NewText,
|
||||||
|
Operation,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
|
@ -490,7 +488,7 @@ 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<WorkflowStep>,
|
patches: Vec<AssistantPatch>,
|
||||||
xml_tags: Vec<XmlTag>,
|
xml_tags: Vec<XmlTag>,
|
||||||
project: Option<Model<Project>>,
|
project: Option<Model<Project>>,
|
||||||
prompt_builder: Arc<PromptBuilder>,
|
prompt_builder: Arc<PromptBuilder>,
|
||||||
|
@ -506,7 +504,7 @@ impl ContextAnnotation for PendingSlashCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContextAnnotation for WorkflowStep {
|
impl ContextAnnotation for AssistantPatch {
|
||||||
fn range(&self) -> &Range<language::Anchor> {
|
fn range(&self) -> &Range<language::Anchor> {
|
||||||
&self.range
|
&self.range
|
||||||
}
|
}
|
||||||
|
@ -591,7 +589,7 @@ impl Context {
|
||||||
telemetry,
|
telemetry,
|
||||||
project,
|
project,
|
||||||
language_registry,
|
language_registry,
|
||||||
workflow_steps: Vec::new(),
|
patches: Vec::new(),
|
||||||
xml_tags: Vec::new(),
|
xml_tags: Vec::new(),
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
};
|
};
|
||||||
|
@ -929,48 +927,49 @@ impl Context {
|
||||||
self.summary.as_ref()
|
self.summary.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn workflow_step_containing(
|
pub(crate) fn patch_containing(
|
||||||
&self,
|
&self,
|
||||||
offset: usize,
|
position: Point,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> Option<&WorkflowStep> {
|
) -> Option<&AssistantPatch> {
|
||||||
let buffer = self.buffer.read(cx);
|
let buffer = self.buffer.read(cx);
|
||||||
let index = self
|
let index = self.patches.binary_search_by(|patch| {
|
||||||
.workflow_steps
|
let patch_range = patch.range.to_point(&buffer);
|
||||||
.binary_search_by(|step| {
|
if position < patch_range.start {
|
||||||
let step_range = step.range.to_offset(&buffer);
|
Ordering::Greater
|
||||||
if offset < step_range.start {
|
} else if position > patch_range.end {
|
||||||
Ordering::Greater
|
Ordering::Less
|
||||||
} else if offset > step_range.end {
|
} else {
|
||||||
Ordering::Less
|
Ordering::Equal
|
||||||
} else {
|
}
|
||||||
Ordering::Equal
|
});
|
||||||
}
|
if let Ok(ix) = index {
|
||||||
})
|
Some(&self.patches[ix])
|
||||||
.ok()?;
|
} else {
|
||||||
Some(&self.workflow_steps[index])
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn workflow_step_ranges(&self) -> impl Iterator<Item = Range<language::Anchor>> + '_ {
|
pub fn patch_ranges(&self) -> impl Iterator<Item = Range<language::Anchor>> + '_ {
|
||||||
self.workflow_steps.iter().map(|step| step.range.clone())
|
self.patches.iter().map(|patch| patch.range.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn workflow_step_for_range(
|
pub(crate) fn patch_for_range(
|
||||||
&self,
|
&self,
|
||||||
range: &Range<language::Anchor>,
|
range: &Range<language::Anchor>,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> Option<&WorkflowStep> {
|
) -> Option<&AssistantPatch> {
|
||||||
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.patch_index_for_range(range, buffer).ok()?;
|
||||||
Some(&self.workflow_steps[index])
|
Some(&self.patches[index])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn workflow_step_index_for_range(
|
fn patch_index_for_range(
|
||||||
&self,
|
&self,
|
||||||
tagged_range: &Range<text::Anchor>,
|
tagged_range: &Range<text::Anchor>,
|
||||||
buffer: &text::BufferSnapshot,
|
buffer: &text::BufferSnapshot,
|
||||||
) -> Result<usize, usize> {
|
) -> Result<usize, usize> {
|
||||||
self.workflow_steps
|
self.patches
|
||||||
.binary_search_by(|probe| probe.range.cmp(&tagged_range, buffer))
|
.binary_search_by(|probe| probe.range.cmp(&tagged_range, buffer))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1018,8 +1017,6 @@ impl Context {
|
||||||
language::BufferEvent::Edited => {
|
language::BufferEvent::Edited => {
|
||||||
self.count_remaining_tokens(cx);
|
self.count_remaining_tokens(cx);
|
||||||
self.reparse(cx);
|
self.reparse(cx);
|
||||||
// Use `inclusive = true` to invalidate a step when an edit occurs
|
|
||||||
// at the start/end of a parsed step.
|
|
||||||
cx.emit(ContextEvent::MessagesEdited);
|
cx.emit(ContextEvent::MessagesEdited);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
@ -1248,8 +1245,8 @@ impl Context {
|
||||||
|
|
||||||
let mut removed_slash_command_ranges = Vec::new();
|
let mut removed_slash_command_ranges = Vec::new();
|
||||||
let mut updated_slash_commands = Vec::new();
|
let mut updated_slash_commands = Vec::new();
|
||||||
let mut removed_steps = Vec::new();
|
let mut removed_patches = Vec::new();
|
||||||
let mut updated_steps = Vec::new();
|
let mut updated_patches = 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 {
|
||||||
|
@ -1273,11 +1270,11 @@ impl Context {
|
||||||
&mut removed_slash_command_ranges,
|
&mut removed_slash_command_ranges,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
self.reparse_workflow_steps_in_range(
|
self.reparse_patches_in_range(
|
||||||
start..end,
|
start..end,
|
||||||
&buffer,
|
&buffer,
|
||||||
&mut updated_steps,
|
&mut updated_patches,
|
||||||
&mut removed_steps,
|
&mut removed_patches,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1289,10 +1286,10 @@ impl Context {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if !updated_steps.is_empty() || !removed_steps.is_empty() {
|
if !updated_patches.is_empty() || !removed_patches.is_empty() {
|
||||||
cx.emit(ContextEvent::WorkflowStepsUpdated {
|
cx.emit(ContextEvent::PatchesUpdated {
|
||||||
removed: removed_steps,
|
removed: removed_patches,
|
||||||
updated: updated_steps,
|
updated: updated_patches,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1354,7 +1351,7 @@ impl Context {
|
||||||
removed.extend(removed_commands.map(|command| command.source_range));
|
removed.extend(removed_commands.map(|command| command.source_range));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reparse_workflow_steps_in_range(
|
fn reparse_patches_in_range(
|
||||||
&mut self,
|
&mut self,
|
||||||
range: Range<text::Anchor>,
|
range: Range<text::Anchor>,
|
||||||
buffer: &BufferSnapshot,
|
buffer: &BufferSnapshot,
|
||||||
|
@ -1369,41 +1366,32 @@ impl Context {
|
||||||
self.xml_tags
|
self.xml_tags
|
||||||
.splice(intersecting_tags_range.clone(), new_tags);
|
.splice(intersecting_tags_range.clone(), new_tags);
|
||||||
|
|
||||||
// Find which steps intersect the changed range.
|
// Find which patches intersect the changed range.
|
||||||
let intersecting_steps_range =
|
let intersecting_patches_range =
|
||||||
self.indices_intersecting_buffer_range(&self.workflow_steps, range.clone(), cx);
|
self.indices_intersecting_buffer_range(&self.patches, range.clone(), cx);
|
||||||
|
|
||||||
// Reparse all tags after the last unchanged step before the change.
|
// Reparse all tags after the last unchanged patch before the change.
|
||||||
let mut tags_start_ix = 0;
|
let mut tags_start_ix = 0;
|
||||||
if let Some(preceding_unchanged_step) =
|
if let Some(preceding_unchanged_patch) =
|
||||||
self.workflow_steps[..intersecting_steps_range.start].last()
|
self.patches[..intersecting_patches_range.start].last()
|
||||||
{
|
{
|
||||||
tags_start_ix = match self.xml_tags.binary_search_by(|tag| {
|
tags_start_ix = match self.xml_tags.binary_search_by(|tag| {
|
||||||
tag.range
|
tag.range
|
||||||
.start
|
.start
|
||||||
.cmp(&preceding_unchanged_step.range.end, buffer)
|
.cmp(&preceding_unchanged_patch.range.end, buffer)
|
||||||
.then(Ordering::Less)
|
.then(Ordering::Less)
|
||||||
}) {
|
}) {
|
||||||
Ok(ix) | Err(ix) => ix,
|
Ok(ix) | Err(ix) => ix,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rebuild the edit suggestions in the range.
|
// Rebuild the patches in the range.
|
||||||
let mut new_steps = self.parse_steps(tags_start_ix, range.end, buffer);
|
let new_patches = self.parse_patches(tags_start_ix, range.end, buffer, cx);
|
||||||
|
updated.extend(new_patches.iter().map(|patch| patch.range.clone()));
|
||||||
if let Some(project) = self.project() {
|
let removed_patches = self.patches.splice(intersecting_patches_range, new_patches);
|
||||||
for step in &mut new_steps {
|
|
||||||
Self::resolve_workflow_step_internal(step, &project, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updated.extend(new_steps.iter().map(|step| step.range.clone()));
|
|
||||||
let removed_steps = self
|
|
||||||
.workflow_steps
|
|
||||||
.splice(intersecting_steps_range, new_steps);
|
|
||||||
removed.extend(
|
removed.extend(
|
||||||
removed_steps
|
removed_patches
|
||||||
.map(|step| step.range)
|
.map(|patch| patch.range)
|
||||||
.filter(|range| !updated.contains(&range)),
|
.filter(|range| !updated.contains(&range)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1464,60 +1452,95 @@ impl Context {
|
||||||
tags
|
tags
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_steps(
|
fn parse_patches(
|
||||||
&mut self,
|
&mut self,
|
||||||
tags_start_ix: usize,
|
tags_start_ix: usize,
|
||||||
buffer_end: text::Anchor,
|
buffer_end: text::Anchor,
|
||||||
buffer: &BufferSnapshot,
|
buffer: &BufferSnapshot,
|
||||||
) -> Vec<WorkflowStep> {
|
cx: &AppContext,
|
||||||
let mut new_steps = Vec::new();
|
) -> Vec<AssistantPatch> {
|
||||||
let mut pending_step = None;
|
let mut new_patches = Vec::new();
|
||||||
let mut edit_step_depth = 0;
|
let mut pending_patch = None;
|
||||||
|
let mut patch_tag_depth = 0;
|
||||||
let mut tags = self.xml_tags[tags_start_ix..].iter().peekable();
|
let mut tags = self.xml_tags[tags_start_ix..].iter().peekable();
|
||||||
'tags: while let Some(tag) = tags.next() {
|
'tags: while let Some(tag) = tags.next() {
|
||||||
if tag.range.start.cmp(&buffer_end, buffer).is_gt() && edit_step_depth == 0 {
|
if tag.range.start.cmp(&buffer_end, buffer).is_gt() && patch_tag_depth == 0 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if tag.kind == XmlTagKind::Step && tag.is_open_tag {
|
if tag.kind == XmlTagKind::Patch && tag.is_open_tag {
|
||||||
edit_step_depth += 1;
|
patch_tag_depth += 1;
|
||||||
let edit_start = tag.range.start;
|
let patch_start = tag.range.start;
|
||||||
let mut edits = Vec::new();
|
let mut edits = Vec::<Result<AssistantEdit>>::new();
|
||||||
let mut step = WorkflowStep {
|
let mut patch = AssistantPatch {
|
||||||
range: edit_start..edit_start,
|
range: patch_start..patch_start,
|
||||||
leading_tags_end: tag.range.end,
|
title: String::new().into(),
|
||||||
trailing_tag_start: None,
|
|
||||||
edits: Default::default(),
|
edits: Default::default(),
|
||||||
resolution: None,
|
status: crate::AssistantPatchStatus::Pending,
|
||||||
resolution_task: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
while let Some(tag) = tags.next() {
|
while let Some(tag) = tags.next() {
|
||||||
step.trailing_tag_start.get_or_insert(tag.range.start);
|
if tag.kind == XmlTagKind::Patch && !tag.is_open_tag {
|
||||||
|
patch_tag_depth -= 1;
|
||||||
|
if patch_tag_depth == 0 {
|
||||||
|
patch.range.end = tag.range.end;
|
||||||
|
|
||||||
if tag.kind == XmlTagKind::Step && !tag.is_open_tag {
|
// Include the line immediately after this <patch> tag if it's empty.
|
||||||
// step.trailing_tag_start = Some(tag.range.start);
|
let patch_end_offset = patch.range.end.to_offset(buffer);
|
||||||
edit_step_depth -= 1;
|
let mut patch_end_chars = buffer.chars_at(patch_end_offset);
|
||||||
if edit_step_depth == 0 {
|
if patch_end_chars.next() == Some('\n')
|
||||||
step.range.end = tag.range.end;
|
&& patch_end_chars.next().map_or(true, |ch| ch == '\n')
|
||||||
step.edits = edits.into();
|
{
|
||||||
new_steps.push(step);
|
let messages = self.messages_for_offsets(
|
||||||
|
[patch_end_offset, patch_end_offset + 1],
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
if messages.len() == 1 {
|
||||||
|
patch.range.end = buffer.anchor_before(patch_end_offset + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
edits.sort_unstable_by(|a, b| {
|
||||||
|
if let (Ok(a), Ok(b)) = (a, b) {
|
||||||
|
a.path.cmp(&b.path)
|
||||||
|
} else {
|
||||||
|
Ordering::Equal
|
||||||
|
}
|
||||||
|
});
|
||||||
|
patch.edits = edits.into();
|
||||||
|
patch.status = AssistantPatchStatus::Ready;
|
||||||
|
new_patches.push(patch);
|
||||||
continue 'tags;
|
continue 'tags;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tag.kind == XmlTagKind::Title && tag.is_open_tag {
|
||||||
|
let content_start = tag.range.end;
|
||||||
|
while let Some(tag) = tags.next() {
|
||||||
|
if tag.kind == XmlTagKind::Title && !tag.is_open_tag {
|
||||||
|
let content_end = tag.range.start;
|
||||||
|
patch.title =
|
||||||
|
trimmed_text_in_range(buffer, content_start..content_end)
|
||||||
|
.into();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if tag.kind == XmlTagKind::Edit && tag.is_open_tag {
|
if tag.kind == XmlTagKind::Edit && tag.is_open_tag {
|
||||||
let mut path = None;
|
let mut path = None;
|
||||||
let mut search = None;
|
let mut old_text = None;
|
||||||
|
let mut new_text = None;
|
||||||
let mut operation = None;
|
let mut operation = None;
|
||||||
let mut description = None;
|
let mut description = None;
|
||||||
|
|
||||||
while let Some(tag) = tags.next() {
|
while let Some(tag) = tags.next() {
|
||||||
if tag.kind == XmlTagKind::Edit && !tag.is_open_tag {
|
if tag.kind == XmlTagKind::Edit && !tag.is_open_tag {
|
||||||
edits.push(WorkflowStepEdit::new(
|
edits.push(AssistantEdit::new(
|
||||||
path,
|
path,
|
||||||
operation,
|
operation,
|
||||||
search,
|
old_text,
|
||||||
|
new_text,
|
||||||
description,
|
description,
|
||||||
));
|
));
|
||||||
break;
|
break;
|
||||||
|
@ -1526,7 +1549,8 @@ impl Context {
|
||||||
if tag.is_open_tag
|
if tag.is_open_tag
|
||||||
&& [
|
&& [
|
||||||
XmlTagKind::Path,
|
XmlTagKind::Path,
|
||||||
XmlTagKind::Search,
|
XmlTagKind::OldText,
|
||||||
|
XmlTagKind::NewText,
|
||||||
XmlTagKind::Operation,
|
XmlTagKind::Operation,
|
||||||
XmlTagKind::Description,
|
XmlTagKind::Description,
|
||||||
]
|
]
|
||||||
|
@ -1538,15 +1562,18 @@ impl Context {
|
||||||
if tag.kind == kind && !tag.is_open_tag {
|
if tag.kind == kind && !tag.is_open_tag {
|
||||||
let tag = tags.next().unwrap();
|
let tag = tags.next().unwrap();
|
||||||
let content_end = tag.range.start;
|
let content_end = tag.range.start;
|
||||||
let mut content = buffer
|
let content = trimmed_text_in_range(
|
||||||
.text_for_range(content_start..content_end)
|
buffer,
|
||||||
.collect::<String>();
|
content_start..content_end,
|
||||||
content.truncate(content.trim_end().len());
|
);
|
||||||
match kind {
|
match kind {
|
||||||
XmlTagKind::Path => path = Some(content),
|
XmlTagKind::Path => path = Some(content),
|
||||||
XmlTagKind::Operation => operation = Some(content),
|
XmlTagKind::Operation => operation = Some(content),
|
||||||
XmlTagKind::Search => {
|
XmlTagKind::OldText => {
|
||||||
search = Some(content).filter(|s| !s.is_empty())
|
old_text = Some(content).filter(|s| !s.is_empty())
|
||||||
|
}
|
||||||
|
XmlTagKind::NewText => {
|
||||||
|
new_text = Some(content).filter(|s| !s.is_empty())
|
||||||
}
|
}
|
||||||
XmlTagKind::Description => {
|
XmlTagKind::Description => {
|
||||||
description =
|
description =
|
||||||
|
@ -1561,162 +1588,28 @@ impl Context {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pending_step = Some(step);
|
patch.edits = edits.into();
|
||||||
|
pending_patch = Some(patch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(mut pending_step) = pending_step {
|
if let Some(mut pending_patch) = pending_patch {
|
||||||
pending_step.range.end = text::Anchor::MAX;
|
let patch_start = pending_patch.range.start.to_offset(buffer);
|
||||||
new_steps.push(pending_step);
|
if let Some(message) = self.message_for_offset(patch_start, cx) {
|
||||||
}
|
if message.anchor_range.end == text::Anchor::MAX {
|
||||||
|
pending_patch.range.end = text::Anchor::MAX;
|
||||||
new_steps
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resolve_workflow_step(
|
|
||||||
&mut self,
|
|
||||||
tagged_range: Range<text::Anchor>,
|
|
||||||
cx: &mut ModelContext<Self>,
|
|
||||||
) -> Option<()> {
|
|
||||||
let index = self
|
|
||||||
.workflow_step_index_for_range(&tagged_range, self.buffer.read(cx))
|
|
||||||
.ok()?;
|
|
||||||
let step = &mut self.workflow_steps[index];
|
|
||||||
let project = self.project.as_ref()?;
|
|
||||||
step.resolution.take();
|
|
||||||
Self::resolve_workflow_step_internal(step, project, cx);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_workflow_step_internal(
|
|
||||||
step: &mut WorkflowStep,
|
|
||||||
project: &Model<Project>,
|
|
||||||
cx: &mut ModelContext<'_, Context>,
|
|
||||||
) {
|
|
||||||
step.resolution_task = Some(cx.spawn({
|
|
||||||
let range = step.range.clone();
|
|
||||||
let edits = step.edits.clone();
|
|
||||||
let project = project.clone();
|
|
||||||
|this, mut cx| async move {
|
|
||||||
let suggestion_groups =
|
|
||||||
Self::compute_step_resolution(project, edits, &mut cx).await;
|
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
let buffer = this.buffer.read(cx).text_snapshot();
|
|
||||||
let ix = this.workflow_step_index_for_range(&range, &buffer).ok();
|
|
||||||
if let Some(ix) = ix {
|
|
||||||
let step = &mut this.workflow_steps[ix];
|
|
||||||
|
|
||||||
let resolution = suggestion_groups.map(|suggestion_groups| {
|
|
||||||
let mut title = String::new();
|
|
||||||
for mut chunk in buffer.text_for_range(
|
|
||||||
step.leading_tags_end
|
|
||||||
..step.trailing_tag_start.unwrap_or(step.range.end),
|
|
||||||
) {
|
|
||||||
if title.is_empty() {
|
|
||||||
chunk = chunk.trim_start();
|
|
||||||
}
|
|
||||||
if let Some((prefix, _)) = chunk.split_once('\n') {
|
|
||||||
title.push_str(prefix);
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
title.push_str(chunk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
WorkflowStepResolution {
|
|
||||||
title,
|
|
||||||
suggestion_groups,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
step.resolution = Some(Arc::new(resolution));
|
|
||||||
cx.emit(ContextEvent::WorkflowStepsUpdated {
|
|
||||||
removed: vec![],
|
|
||||||
updated: vec![range],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn compute_step_resolution(
|
|
||||||
project: Model<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 {
|
} else {
|
||||||
// Create the first group
|
let message_end = buffer.anchor_after(message.offset_range.end - 1);
|
||||||
suggestion_groups.push(WorkflowSuggestionGroup {
|
pending_patch.range.end = message_end;
|
||||||
context_range,
|
|
||||||
suggestions: vec![suggestion],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
pending_patch.range.end = text::Anchor::MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
suggestion_groups_by_buffer.insert(buffer, suggestion_groups);
|
new_patches.push(pending_patch);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(suggestion_groups_by_buffer)
|
new_patches
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pending_command_for_position(
|
pub fn pending_command_for_position(
|
||||||
|
@ -2315,11 +2208,11 @@ impl Context {
|
||||||
let mut updated = Vec::new();
|
let mut updated = Vec::new();
|
||||||
let mut removed = Vec::new();
|
let mut removed = Vec::new();
|
||||||
for range in ranges {
|
for range in ranges {
|
||||||
self.reparse_workflow_steps_in_range(range, &buffer, &mut updated, &mut removed, cx);
|
self.reparse_patches_in_range(range, &buffer, &mut updated, &mut removed, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !updated.is_empty() || !removed.is_empty() {
|
if !updated.is_empty() || !removed.is_empty() {
|
||||||
cx.emit(ContextEvent::WorkflowStepsUpdated { removed, updated })
|
cx.emit(ContextEvent::PatchesUpdated { removed, updated })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2825,6 +2718,24 @@ impl Context {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn trimmed_text_in_range(buffer: &BufferSnapshot, range: Range<text::Anchor>) -> String {
|
||||||
|
let mut is_start = true;
|
||||||
|
let mut content = buffer
|
||||||
|
.text_for_range(range)
|
||||||
|
.map(|mut chunk| {
|
||||||
|
if is_start {
|
||||||
|
chunk = chunk.trim_start_matches('\n');
|
||||||
|
if !chunk.is_empty() {
|
||||||
|
is_start = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chunk
|
||||||
|
})
|
||||||
|
.collect::<String>();
|
||||||
|
content.truncate(content.trim_end().len());
|
||||||
|
content
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct ContextVersion {
|
pub struct ContextVersion {
|
||||||
context: clock::Global,
|
context: clock::Global,
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
use super::{MessageCacheMetadata, WorkflowStepEdit};
|
use super::{AssistantEdit, MessageCacheMetadata};
|
||||||
use crate::{
|
use crate::{
|
||||||
assistant_panel, prompt_library, slash_command::file_command, CacheStatus, Context,
|
assistant_panel, prompt_library, slash_command::file_command, AssistantEditKind, CacheStatus,
|
||||||
ContextEvent, ContextId, ContextOperation, MessageId, MessageStatus, PromptBuilder,
|
Context, ContextEvent, ContextId, ContextOperation, MessageId, MessageStatus, PromptBuilder,
|
||||||
WorkflowStepEditKind,
|
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use assistant_slash_command::{
|
use assistant_slash_command::{
|
||||||
|
@ -15,6 +14,7 @@ use gpui::{AppContext, Model, SharedString, Task, TestAppContext, WeakView};
|
||||||
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
|
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
|
||||||
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
|
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
@ -478,7 +478,15 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
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 mut settings_store = cx.update(SettingsStore::test);
|
||||||
|
cx.update(|cx| {
|
||||||
|
settings_store
|
||||||
|
.set_user_settings(
|
||||||
|
r#"{ "assistant": { "enable_experimental_live_diffs": true } }"#,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
cx.update(language::init);
|
cx.update(language::init);
|
||||||
cx.update(Project::init_settings);
|
cx.update(Project::init_settings);
|
||||||
|
@ -520,7 +528,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||||
»",
|
»",
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
expect_steps(
|
expect_patches(
|
||||||
&context,
|
&context,
|
||||||
"
|
"
|
||||||
|
|
||||||
|
@ -539,17 +547,17 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||||
one
|
one
|
||||||
two
|
two
|
||||||
«
|
«
|
||||||
<step»",
|
<patch»",
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
expect_steps(
|
expect_patches(
|
||||||
&context,
|
&context,
|
||||||
"
|
"
|
||||||
|
|
||||||
one
|
one
|
||||||
two
|
two
|
||||||
|
|
||||||
<step",
|
<patch",
|
||||||
&[],
|
&[],
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
@ -563,36 +571,24 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||||
one
|
one
|
||||||
two
|
two
|
||||||
|
|
||||||
<step«>
|
<patch«>
|
||||||
Add a second function
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn two() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
<edit>»",
|
<edit>»",
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
expect_steps(
|
expect_patches(
|
||||||
&context,
|
&context,
|
||||||
"
|
"
|
||||||
|
|
||||||
one
|
one
|
||||||
two
|
two
|
||||||
|
|
||||||
«<step>
|
«<patch>
|
||||||
Add a second function
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn two() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
<edit>»",
|
<edit>»",
|
||||||
&[&[]],
|
&[&[]],
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
// The full suggestion is added
|
// The full patch is added
|
||||||
edit(
|
edit(
|
||||||
&context,
|
&context,
|
||||||
"
|
"
|
||||||
|
@ -600,51 +596,46 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||||
one
|
one
|
||||||
two
|
two
|
||||||
|
|
||||||
<step>
|
<patch>
|
||||||
Add a second function
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn two() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
<edit>«
|
<edit>«
|
||||||
|
<description>add a `two` function</description>
|
||||||
<path>src/lib.rs</path>
|
<path>src/lib.rs</path>
|
||||||
<operation>insert_after</operation>
|
<operation>insert_after</operation>
|
||||||
<search>fn one</search>
|
<old_text>fn one</old_text>
|
||||||
<description>add a `two` function</description>
|
<new_text>
|
||||||
|
fn two() {}
|
||||||
|
</new_text>
|
||||||
</edit>
|
</edit>
|
||||||
</step>
|
</patch>
|
||||||
|
|
||||||
also,»",
|
also,»",
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
expect_steps(
|
expect_patches(
|
||||||
&context,
|
&context,
|
||||||
"
|
"
|
||||||
|
|
||||||
one
|
one
|
||||||
two
|
two
|
||||||
|
|
||||||
«<step>
|
«<patch>
|
||||||
Add a second function
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn two() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
<edit>
|
<edit>
|
||||||
|
<description>add a `two` function</description>
|
||||||
<path>src/lib.rs</path>
|
<path>src/lib.rs</path>
|
||||||
<operation>insert_after</operation>
|
<operation>insert_after</operation>
|
||||||
<search>fn one</search>
|
<old_text>fn one</old_text>
|
||||||
<description>add a `two` function</description>
|
<new_text>
|
||||||
|
fn two() {}
|
||||||
|
</new_text>
|
||||||
</edit>
|
</edit>
|
||||||
</step>»
|
</patch>
|
||||||
|
»
|
||||||
also,",
|
also,",
|
||||||
&[&[WorkflowStepEdit {
|
&[&[AssistantEdit {
|
||||||
path: "src/lib.rs".into(),
|
path: "src/lib.rs".into(),
|
||||||
kind: WorkflowStepEditKind::InsertAfter {
|
kind: AssistantEditKind::InsertAfter {
|
||||||
search: "fn one".into(),
|
old_text: "fn one".into(),
|
||||||
|
new_text: "fn two() {}".into(),
|
||||||
description: "add a `two` function".into(),
|
description: "add a `two` function".into(),
|
||||||
},
|
},
|
||||||
}]],
|
}]],
|
||||||
|
@ -659,51 +650,46 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||||
one
|
one
|
||||||
two
|
two
|
||||||
|
|
||||||
<step>
|
<patch>
|
||||||
Add a second function
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn two() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
<edit>
|
<edit>
|
||||||
|
<description>add a `two` function</description>
|
||||||
<path>src/lib.rs</path>
|
<path>src/lib.rs</path>
|
||||||
<operation>insert_after</operation>
|
<operation>insert_after</operation>
|
||||||
<search>«fn zero»</search>
|
<old_text>«fn zero»</old_text>
|
||||||
<description>add a `two` function</description>
|
<new_text>
|
||||||
|
fn two() {}
|
||||||
|
</new_text>
|
||||||
</edit>
|
</edit>
|
||||||
</step>
|
</patch>
|
||||||
|
|
||||||
also,",
|
also,",
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
expect_steps(
|
expect_patches(
|
||||||
&context,
|
&context,
|
||||||
"
|
"
|
||||||
|
|
||||||
one
|
one
|
||||||
two
|
two
|
||||||
|
|
||||||
«<step>
|
«<patch>
|
||||||
Add a second function
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn two() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
<edit>
|
<edit>
|
||||||
|
<description>add a `two` function</description>
|
||||||
<path>src/lib.rs</path>
|
<path>src/lib.rs</path>
|
||||||
<operation>insert_after</operation>
|
<operation>insert_after</operation>
|
||||||
<search>fn zero</search>
|
<old_text>fn zero</old_text>
|
||||||
<description>add a `two` function</description>
|
<new_text>
|
||||||
|
fn two() {}
|
||||||
|
</new_text>
|
||||||
</edit>
|
</edit>
|
||||||
</step>»
|
</patch>
|
||||||
|
»
|
||||||
also,",
|
also,",
|
||||||
&[&[WorkflowStepEdit {
|
&[&[AssistantEdit {
|
||||||
path: "src/lib.rs".into(),
|
path: "src/lib.rs".into(),
|
||||||
kind: WorkflowStepEditKind::InsertAfter {
|
kind: AssistantEditKind::InsertAfter {
|
||||||
search: "fn zero".into(),
|
old_text: "fn zero".into(),
|
||||||
|
new_text: "fn two() {}".into(),
|
||||||
description: "add a `two` function".into(),
|
description: "add a `two` function".into(),
|
||||||
},
|
},
|
||||||
}]],
|
}]],
|
||||||
|
@ -715,27 +701,24 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||||
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
|
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
|
||||||
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
|
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
|
||||||
});
|
});
|
||||||
expect_steps(
|
expect_patches(
|
||||||
&context,
|
&context,
|
||||||
"
|
"
|
||||||
|
|
||||||
one
|
one
|
||||||
two
|
two
|
||||||
|
|
||||||
<step>
|
<patch>
|
||||||
Add a second function
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn two() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
<edit>
|
<edit>
|
||||||
|
<description>add a `two` function</description>
|
||||||
<path>src/lib.rs</path>
|
<path>src/lib.rs</path>
|
||||||
<operation>insert_after</operation>
|
<operation>insert_after</operation>
|
||||||
<search>fn zero</search>
|
<old_text>fn zero</old_text>
|
||||||
<description>add a `two` function</description>
|
<new_text>
|
||||||
|
fn two() {}
|
||||||
|
</new_text>
|
||||||
</edit>
|
</edit>
|
||||||
</step>
|
</patch>
|
||||||
|
|
||||||
also,",
|
also,",
|
||||||
&[],
|
&[],
|
||||||
|
@ -746,33 +729,31 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||||
context.update(cx, |context, cx| {
|
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(
|
expect_patches(
|
||||||
&context,
|
&context,
|
||||||
"
|
"
|
||||||
|
|
||||||
one
|
one
|
||||||
two
|
two
|
||||||
|
|
||||||
«<step>
|
«<patch>
|
||||||
Add a second function
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn two() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
<edit>
|
<edit>
|
||||||
|
<description>add a `two` function</description>
|
||||||
<path>src/lib.rs</path>
|
<path>src/lib.rs</path>
|
||||||
<operation>insert_after</operation>
|
<operation>insert_after</operation>
|
||||||
<search>fn zero</search>
|
<old_text>fn zero</old_text>
|
||||||
<description>add a `two` function</description>
|
<new_text>
|
||||||
|
fn two() {}
|
||||||
|
</new_text>
|
||||||
</edit>
|
</edit>
|
||||||
</step>»
|
</patch>
|
||||||
|
»
|
||||||
also,",
|
also,",
|
||||||
&[&[WorkflowStepEdit {
|
&[&[AssistantEdit {
|
||||||
path: "src/lib.rs".into(),
|
path: "src/lib.rs".into(),
|
||||||
kind: WorkflowStepEditKind::InsertAfter {
|
kind: AssistantEditKind::InsertAfter {
|
||||||
search: "fn zero".into(),
|
old_text: "fn zero".into(),
|
||||||
|
new_text: "fn two() {}".into(),
|
||||||
description: "add a `two` function".into(),
|
description: "add a `two` function".into(),
|
||||||
},
|
},
|
||||||
}]],
|
}]],
|
||||||
|
@ -792,33 +773,31 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
expect_steps(
|
expect_patches(
|
||||||
&deserialized_context,
|
&deserialized_context,
|
||||||
"
|
"
|
||||||
|
|
||||||
one
|
one
|
||||||
two
|
two
|
||||||
|
|
||||||
«<step>
|
«<patch>
|
||||||
Add a second function
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn two() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
<edit>
|
<edit>
|
||||||
|
<description>add a `two` function</description>
|
||||||
<path>src/lib.rs</path>
|
<path>src/lib.rs</path>
|
||||||
<operation>insert_after</operation>
|
<operation>insert_after</operation>
|
||||||
<search>fn zero</search>
|
<old_text>fn zero</old_text>
|
||||||
<description>add a `two` function</description>
|
<new_text>
|
||||||
|
fn two() {}
|
||||||
|
</new_text>
|
||||||
</edit>
|
</edit>
|
||||||
</step>»
|
</patch>
|
||||||
|
»
|
||||||
also,",
|
also,",
|
||||||
&[&[WorkflowStepEdit {
|
&[&[AssistantEdit {
|
||||||
path: "src/lib.rs".into(),
|
path: "src/lib.rs".into(),
|
||||||
kind: WorkflowStepEditKind::InsertAfter {
|
kind: AssistantEditKind::InsertAfter {
|
||||||
search: "fn zero".into(),
|
old_text: "fn zero".into(),
|
||||||
|
new_text: "fn two() {}".into(),
|
||||||
description: "add a `two` function".into(),
|
description: "add a `two` function".into(),
|
||||||
},
|
},
|
||||||
}]],
|
}]],
|
||||||
|
@ -834,48 +813,58 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||||
cx.executor().run_until_parked();
|
cx.executor().run_until_parked();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expect_steps(
|
#[track_caller]
|
||||||
|
fn expect_patches(
|
||||||
context: &Model<Context>,
|
context: &Model<Context>,
|
||||||
expected_marked_text: &str,
|
expected_marked_text: &str,
|
||||||
expected_suggestions: &[&[WorkflowStepEdit]],
|
expected_suggestions: &[&[AssistantEdit]],
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) {
|
) {
|
||||||
context.update(cx, |context, cx| {
|
let expected_marked_text = expected_marked_text.unindent();
|
||||||
let expected_marked_text = expected_marked_text.unindent();
|
let (expected_text, _) = marked_text_ranges(&expected_marked_text, false);
|
||||||
let (expected_text, expected_ranges) = marked_text_ranges(&expected_marked_text, false);
|
|
||||||
|
let (buffer_text, ranges, patches) = context.update(cx, |context, cx| {
|
||||||
context.buffer.read_with(cx, |buffer, _| {
|
context.buffer.read_with(cx, |buffer, _| {
|
||||||
assert_eq!(buffer.text(), expected_text);
|
|
||||||
let ranges = context
|
let ranges = context
|
||||||
.workflow_steps
|
.patches
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entry| entry.range.to_offset(buffer))
|
.map(|entry| entry.range.to_offset(buffer))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let marked = generate_marked_text(&expected_text, &ranges, false);
|
(
|
||||||
assert_eq!(
|
buffer.text(),
|
||||||
marked,
|
ranges,
|
||||||
expected_marked_text,
|
context
|
||||||
"unexpected suggestion ranges. actual: {ranges:?}, expected: {expected_ranges:?}"
|
.patches
|
||||||
);
|
.iter()
|
||||||
let suggestions = context
|
.map(|step| step.edits.clone())
|
||||||
.workflow_steps
|
.collect::<Vec<_>>(),
|
||||||
.iter()
|
)
|
||||||
.map(|step| {
|
})
|
||||||
step.edits
|
|
||||||
.iter()
|
|
||||||
.map(|edit| {
|
|
||||||
let edit = edit.as_ref().unwrap();
|
|
||||||
WorkflowStepEdit {
|
|
||||||
path: edit.path.clone(),
|
|
||||||
kind: edit.kind.clone(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
assert_eq!(suggestions, expected_suggestions);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
assert_eq!(buffer_text, expected_text);
|
||||||
|
|
||||||
|
let actual_marked_text = generate_marked_text(&expected_text, &ranges, false);
|
||||||
|
assert_eq!(actual_marked_text, expected_marked_text);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
patches
|
||||||
|
.iter()
|
||||||
|
.map(|patch| {
|
||||||
|
patch
|
||||||
|
.iter()
|
||||||
|
.map(|edit| {
|
||||||
|
let edit = edit.as_ref().unwrap();
|
||||||
|
AssistantEdit {
|
||||||
|
path: edit.path.clone(),
|
||||||
|
kind: edit.kind.clone(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
expected_suggestions
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,13 +82,6 @@ pub struct InlineAssistant {
|
||||||
assists: HashMap<InlineAssistId, InlineAssist>,
|
assists: HashMap<InlineAssistId, InlineAssist>,
|
||||||
assists_by_editor: HashMap<WeakView<Editor>, EditorInlineAssists>,
|
assists_by_editor: HashMap<WeakView<Editor>, EditorInlineAssists>,
|
||||||
assist_groups: HashMap<InlineAssistGroupId, InlineAssistGroup>,
|
assist_groups: HashMap<InlineAssistGroupId, InlineAssistGroup>,
|
||||||
assist_observations: HashMap<
|
|
||||||
InlineAssistId,
|
|
||||||
(
|
|
||||||
async_watch::Sender<AssistStatus>,
|
|
||||||
async_watch::Receiver<AssistStatus>,
|
|
||||||
),
|
|
||||||
>,
|
|
||||||
confirmed_assists: HashMap<InlineAssistId, Model<CodegenAlternative>>,
|
confirmed_assists: HashMap<InlineAssistId, Model<CodegenAlternative>>,
|
||||||
prompt_history: VecDeque<String>,
|
prompt_history: VecDeque<String>,
|
||||||
prompt_builder: Arc<PromptBuilder>,
|
prompt_builder: Arc<PromptBuilder>,
|
||||||
|
@ -96,19 +89,6 @@ pub struct InlineAssistant {
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum AssistStatus {
|
|
||||||
Idle,
|
|
||||||
Started,
|
|
||||||
Stopped,
|
|
||||||
Finished,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AssistStatus {
|
|
||||||
pub fn is_done(&self) -> bool {
|
|
||||||
matches!(self, Self::Stopped | Self::Finished)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Global for InlineAssistant {}
|
impl Global for InlineAssistant {}
|
||||||
|
|
||||||
impl InlineAssistant {
|
impl InlineAssistant {
|
||||||
|
@ -123,7 +103,6 @@ impl InlineAssistant {
|
||||||
assists: HashMap::default(),
|
assists: HashMap::default(),
|
||||||
assists_by_editor: HashMap::default(),
|
assists_by_editor: HashMap::default(),
|
||||||
assist_groups: HashMap::default(),
|
assist_groups: HashMap::default(),
|
||||||
assist_observations: HashMap::default(),
|
|
||||||
confirmed_assists: HashMap::default(),
|
confirmed_assists: HashMap::default(),
|
||||||
prompt_history: VecDeque::default(),
|
prompt_history: VecDeque::default(),
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
|
@ -835,17 +814,6 @@ impl InlineAssistant {
|
||||||
.insert(assist_id, confirmed_alternative);
|
.insert(assist_id, confirmed_alternative);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the assist from the status updates map
|
|
||||||
self.assist_observations.remove(&assist_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn undo_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool {
|
|
||||||
let Some(codegen) = self.confirmed_assists.remove(&assist_id) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
codegen.update(cx, |this, cx| this.undo(cx));
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dismiss_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool {
|
fn dismiss_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool {
|
||||||
|
@ -1039,10 +1007,6 @@ impl InlineAssistant {
|
||||||
codegen.start(user_prompt, assistant_panel_context, cx)
|
codegen.start(user_prompt, assistant_panel_context, cx)
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
|
|
||||||
if let Some((tx, _)) = self.assist_observations.get(&assist_id) {
|
|
||||||
tx.send(AssistStatus::Started).ok();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
|
pub fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
|
||||||
|
@ -1053,25 +1017,6 @@ impl InlineAssistant {
|
||||||
};
|
};
|
||||||
|
|
||||||
assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
|
assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
|
||||||
|
|
||||||
if let Some((tx, _)) = self.assist_observations.get(&assist_id) {
|
|
||||||
tx.send(AssistStatus::Stopped).ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assist_status(&self, assist_id: InlineAssistId, cx: &AppContext) -> InlineAssistStatus {
|
|
||||||
if let Some(assist) = self.assists.get(&assist_id) {
|
|
||||||
match assist.codegen.read(cx).status(cx) {
|
|
||||||
CodegenStatus::Idle => InlineAssistStatus::Idle,
|
|
||||||
CodegenStatus::Pending => InlineAssistStatus::Pending,
|
|
||||||
CodegenStatus::Done => InlineAssistStatus::Done,
|
|
||||||
CodegenStatus::Error(_) => InlineAssistStatus::Error,
|
|
||||||
}
|
|
||||||
} else if self.confirmed_assists.contains_key(&assist_id) {
|
|
||||||
InlineAssistStatus::Confirmed
|
|
||||||
} else {
|
|
||||||
InlineAssistStatus::Canceled
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_editor_highlights(&self, editor: &View<Editor>, cx: &mut WindowContext) {
|
fn update_editor_highlights(&self, editor: &View<Editor>, cx: &mut WindowContext) {
|
||||||
|
@ -1257,42 +1202,6 @@ impl InlineAssistant {
|
||||||
.collect();
|
.collect();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn observe_assist(
|
|
||||||
&mut self,
|
|
||||||
assist_id: InlineAssistId,
|
|
||||||
) -> async_watch::Receiver<AssistStatus> {
|
|
||||||
if let Some((_, rx)) = self.assist_observations.get(&assist_id) {
|
|
||||||
rx.clone()
|
|
||||||
} else {
|
|
||||||
let (tx, rx) = async_watch::channel(AssistStatus::Idle);
|
|
||||||
self.assist_observations.insert(assist_id, (tx, rx.clone()));
|
|
||||||
rx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum InlineAssistStatus {
|
|
||||||
Idle,
|
|
||||||
Pending,
|
|
||||||
Done,
|
|
||||||
Error,
|
|
||||||
Confirmed,
|
|
||||||
Canceled,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InlineAssistStatus {
|
|
||||||
pub(crate) fn is_pending(&self) -> bool {
|
|
||||||
matches!(self, Self::Pending)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn is_confirmed(&self) -> bool {
|
|
||||||
matches!(self, Self::Confirmed)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn is_done(&self) -> bool {
|
|
||||||
matches!(self, Self::Done)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct EditorInlineAssists {
|
struct EditorInlineAssists {
|
||||||
|
@ -2290,8 +2199,6 @@ impl InlineAssist {
|
||||||
|
|
||||||
if assist.decorations.is_none() {
|
if assist.decorations.is_none() {
|
||||||
this.finish_assist(assist_id, false, cx);
|
this.finish_assist(assist_id, false, cx);
|
||||||
} else if let Some(tx) = this.assist_observations.get(&assist_id) {
|
|
||||||
tx.0.send(AssistStatus::Finished).ok();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
746
crates/assistant/src/patch.rs
Normal file
746
crates/assistant/src/patch.rs
Normal file
|
@ -0,0 +1,746 @@
|
||||||
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
|
use collections::HashMap;
|
||||||
|
use editor::ProposedChangesEditor;
|
||||||
|
use futures::{future, TryFutureExt as _};
|
||||||
|
use gpui::{AppContext, AsyncAppContext, Model, SharedString};
|
||||||
|
use language::{AutoindentMode, Buffer, BufferSnapshot};
|
||||||
|
use project::{Project, ProjectPath};
|
||||||
|
use std::{cmp, ops::Range, path::Path, sync::Arc};
|
||||||
|
use text::{AnchorRangeExt as _, Bias, OffsetRangeExt as _, Point};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct AssistantPatch {
|
||||||
|
pub range: Range<language::Anchor>,
|
||||||
|
pub title: SharedString,
|
||||||
|
pub edits: Arc<[Result<AssistantEdit>]>,
|
||||||
|
pub status: AssistantPatchStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) enum AssistantPatchStatus {
|
||||||
|
Pending,
|
||||||
|
Ready,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) struct AssistantEdit {
|
||||||
|
pub path: String,
|
||||||
|
pub kind: AssistantEditKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum AssistantEditKind {
|
||||||
|
Update {
|
||||||
|
old_text: String,
|
||||||
|
new_text: String,
|
||||||
|
description: String,
|
||||||
|
},
|
||||||
|
Create {
|
||||||
|
new_text: String,
|
||||||
|
description: String,
|
||||||
|
},
|
||||||
|
InsertBefore {
|
||||||
|
old_text: String,
|
||||||
|
new_text: String,
|
||||||
|
description: String,
|
||||||
|
},
|
||||||
|
InsertAfter {
|
||||||
|
old_text: String,
|
||||||
|
new_text: String,
|
||||||
|
description: String,
|
||||||
|
},
|
||||||
|
Delete {
|
||||||
|
old_text: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub(crate) struct ResolvedPatch {
|
||||||
|
pub edit_groups: HashMap<Model<Buffer>, Vec<ResolvedEditGroup>>,
|
||||||
|
pub errors: Vec<AssistantPatchResolutionError>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct ResolvedEditGroup {
|
||||||
|
pub context_range: Range<language::Anchor>,
|
||||||
|
pub edits: Vec<ResolvedEdit>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct ResolvedEdit {
|
||||||
|
range: Range<language::Anchor>,
|
||||||
|
new_text: String,
|
||||||
|
description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub(crate) struct AssistantPatchResolutionError {
|
||||||
|
pub edit_ix: usize,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
enum SearchDirection {
|
||||||
|
Up,
|
||||||
|
Left,
|
||||||
|
Diagonal,
|
||||||
|
}
|
||||||
|
|
||||||
|
// A measure of the currently quality of an in-progress fuzzy search.
|
||||||
|
//
|
||||||
|
// Uses 60 bits to store a numeric cost, and 4 bits to store the preceding
|
||||||
|
// operation in the search.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
struct SearchState {
|
||||||
|
score: u32,
|
||||||
|
direction: SearchDirection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchState {
|
||||||
|
fn new(score: u32, direction: SearchDirection) -> Self {
|
||||||
|
Self { score, direction }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResolvedPatch {
|
||||||
|
pub fn apply(&self, editor: &ProposedChangesEditor, cx: &mut AppContext) {
|
||||||
|
for (buffer, groups) in &self.edit_groups {
|
||||||
|
let branch = editor.branch_buffer_for_base(buffer).unwrap();
|
||||||
|
Self::apply_edit_groups(groups, &branch, cx);
|
||||||
|
}
|
||||||
|
editor.recalculate_all_buffer_diffs();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_edit_groups(
|
||||||
|
groups: &Vec<ResolvedEditGroup>,
|
||||||
|
buffer: &Model<Buffer>,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) {
|
||||||
|
let mut edits = Vec::new();
|
||||||
|
for group in groups {
|
||||||
|
for suggestion in &group.edits {
|
||||||
|
edits.push((suggestion.range.clone(), suggestion.new_text.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.edit(
|
||||||
|
edits,
|
||||||
|
Some(AutoindentMode::Block {
|
||||||
|
original_indent_columns: Vec::new(),
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResolvedEdit {
|
||||||
|
pub fn try_merge(&mut self, other: &Self, buffer: &text::BufferSnapshot) -> bool {
|
||||||
|
let range = &self.range;
|
||||||
|
let other_range = &other.range;
|
||||||
|
|
||||||
|
// Don't merge if we don't contain the other suggestion.
|
||||||
|
if range.start.cmp(&other_range.start, buffer).is_gt()
|
||||||
|
|| range.end.cmp(&other_range.end, buffer).is_lt()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(description) = &mut self.description {
|
||||||
|
if let Some(other_description) = &other.description {
|
||||||
|
description.push('\n');
|
||||||
|
description.push_str(other_description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssistantEdit {
|
||||||
|
pub fn new(
|
||||||
|
path: Option<String>,
|
||||||
|
operation: Option<String>,
|
||||||
|
old_text: Option<String>,
|
||||||
|
new_text: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let path = path.ok_or_else(|| anyhow!("missing path"))?;
|
||||||
|
let operation = operation.ok_or_else(|| anyhow!("missing operation"))?;
|
||||||
|
|
||||||
|
let kind = match operation.as_str() {
|
||||||
|
"update" => AssistantEditKind::Update {
|
||||||
|
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
|
||||||
|
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
|
||||||
|
description: description.ok_or_else(|| anyhow!("missing description"))?,
|
||||||
|
},
|
||||||
|
"insert_before" => AssistantEditKind::InsertBefore {
|
||||||
|
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
|
||||||
|
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
|
||||||
|
description: description.ok_or_else(|| anyhow!("missing description"))?,
|
||||||
|
},
|
||||||
|
"insert_after" => AssistantEditKind::InsertAfter {
|
||||||
|
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
|
||||||
|
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
|
||||||
|
description: description.ok_or_else(|| anyhow!("missing description"))?,
|
||||||
|
},
|
||||||
|
"delete" => AssistantEditKind::Delete {
|
||||||
|
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
|
||||||
|
},
|
||||||
|
"create" => AssistantEditKind::Create {
|
||||||
|
description: description.ok_or_else(|| anyhow!("missing description"))?,
|
||||||
|
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
|
||||||
|
},
|
||||||
|
_ => Err(anyhow!("unknown operation {operation:?}"))?,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self { path, kind })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn resolve(
|
||||||
|
&self,
|
||||||
|
project: Model<Project>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<(Model<Buffer>, ResolvedEdit)> {
|
||||||
|
let path = self.path.clone();
|
||||||
|
let kind = self.kind.clone();
|
||||||
|
let buffer = project
|
||||||
|
.update(&mut cx, |project, cx| {
|
||||||
|
let project_path = project
|
||||||
|
.find_project_path(Path::new(&path), cx)
|
||||||
|
.or_else(|| {
|
||||||
|
// If we couldn't find a project path for it, put it in the active worktree
|
||||||
|
// so that when we create the buffer, it can be saved.
|
||||||
|
let worktree = project
|
||||||
|
.active_entry()
|
||||||
|
.and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
|
||||||
|
.or_else(|| project.worktrees(cx).next())?;
|
||||||
|
let worktree = worktree.read(cx);
|
||||||
|
|
||||||
|
Some(ProjectPath {
|
||||||
|
worktree_id: worktree.id(),
|
||||||
|
path: Arc::from(Path::new(&path)),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.with_context(|| format!("worktree not found for {:?}", path))?;
|
||||||
|
anyhow::Ok(project.open_buffer(project_path, cx))
|
||||||
|
})??
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
|
||||||
|
let suggestion = cx
|
||||||
|
.background_executor()
|
||||||
|
.spawn(async move { kind.resolve(&snapshot) })
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok((buffer, suggestion))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssistantEditKind {
|
||||||
|
fn resolve(self, snapshot: &BufferSnapshot) -> ResolvedEdit {
|
||||||
|
match self {
|
||||||
|
Self::Update {
|
||||||
|
old_text,
|
||||||
|
new_text,
|
||||||
|
description,
|
||||||
|
} => {
|
||||||
|
let range = Self::resolve_location(&snapshot, &old_text);
|
||||||
|
ResolvedEdit {
|
||||||
|
range,
|
||||||
|
new_text,
|
||||||
|
description: Some(description),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Create {
|
||||||
|
new_text,
|
||||||
|
description,
|
||||||
|
} => ResolvedEdit {
|
||||||
|
range: text::Anchor::MIN..text::Anchor::MAX,
|
||||||
|
description: Some(description),
|
||||||
|
new_text,
|
||||||
|
},
|
||||||
|
Self::InsertBefore {
|
||||||
|
old_text,
|
||||||
|
mut new_text,
|
||||||
|
description,
|
||||||
|
} => {
|
||||||
|
let range = Self::resolve_location(&snapshot, &old_text);
|
||||||
|
new_text.push('\n');
|
||||||
|
ResolvedEdit {
|
||||||
|
range: range.start..range.start,
|
||||||
|
new_text,
|
||||||
|
description: Some(description),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::InsertAfter {
|
||||||
|
old_text,
|
||||||
|
mut new_text,
|
||||||
|
description,
|
||||||
|
} => {
|
||||||
|
let range = Self::resolve_location(&snapshot, &old_text);
|
||||||
|
new_text.insert(0, '\n');
|
||||||
|
ResolvedEdit {
|
||||||
|
range: range.end..range.end,
|
||||||
|
new_text,
|
||||||
|
description: Some(description),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Delete { old_text } => {
|
||||||
|
let range = Self::resolve_location(&snapshot, &old_text);
|
||||||
|
ResolvedEdit {
|
||||||
|
range,
|
||||||
|
new_text: String::new(),
|
||||||
|
description: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_location(buffer: &text::BufferSnapshot, search_query: &str) -> Range<text::Anchor> {
|
||||||
|
const INSERTION_COST: u32 = 3;
|
||||||
|
const WHITESPACE_INSERTION_COST: u32 = 1;
|
||||||
|
const DELETION_COST: u32 = 3;
|
||||||
|
const WHITESPACE_DELETION_COST: u32 = 1;
|
||||||
|
const EQUALITY_BONUS: u32 = 5;
|
||||||
|
|
||||||
|
struct Matrix {
|
||||||
|
cols: usize,
|
||||||
|
data: Vec<SearchState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Matrix {
|
||||||
|
fn new(rows: usize, cols: usize) -> Self {
|
||||||
|
Matrix {
|
||||||
|
cols,
|
||||||
|
data: vec![SearchState::new(0, SearchDirection::Diagonal); rows * cols],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self, row: usize, col: usize) -> SearchState {
|
||||||
|
self.data[row * self.cols + col]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set(&mut self, row: usize, col: usize, cost: SearchState) {
|
||||||
|
self.data[row * self.cols + col] = cost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let buffer_len = buffer.len();
|
||||||
|
let query_len = search_query.len();
|
||||||
|
let mut matrix = Matrix::new(query_len + 1, buffer_len + 1);
|
||||||
|
|
||||||
|
for (row, query_byte) in search_query.bytes().enumerate() {
|
||||||
|
for (col, buffer_byte) in buffer.bytes_in_range(0..buffer.len()).flatten().enumerate() {
|
||||||
|
let deletion_cost = if query_byte.is_ascii_whitespace() {
|
||||||
|
WHITESPACE_DELETION_COST
|
||||||
|
} else {
|
||||||
|
DELETION_COST
|
||||||
|
};
|
||||||
|
let insertion_cost = if buffer_byte.is_ascii_whitespace() {
|
||||||
|
WHITESPACE_INSERTION_COST
|
||||||
|
} else {
|
||||||
|
INSERTION_COST
|
||||||
|
};
|
||||||
|
|
||||||
|
let up = SearchState::new(
|
||||||
|
matrix.get(row, col + 1).score.saturating_sub(deletion_cost),
|
||||||
|
SearchDirection::Up,
|
||||||
|
);
|
||||||
|
let left = SearchState::new(
|
||||||
|
matrix
|
||||||
|
.get(row + 1, col)
|
||||||
|
.score
|
||||||
|
.saturating_sub(insertion_cost),
|
||||||
|
SearchDirection::Left,
|
||||||
|
);
|
||||||
|
let diagonal = SearchState::new(
|
||||||
|
if query_byte == *buffer_byte {
|
||||||
|
matrix.get(row, col).score.saturating_add(EQUALITY_BONUS)
|
||||||
|
} else {
|
||||||
|
matrix
|
||||||
|
.get(row, col)
|
||||||
|
.score
|
||||||
|
.saturating_sub(deletion_cost + insertion_cost)
|
||||||
|
},
|
||||||
|
SearchDirection::Diagonal,
|
||||||
|
);
|
||||||
|
matrix.set(row + 1, col + 1, up.max(left).max(diagonal));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traceback to find the best match
|
||||||
|
let mut best_buffer_end = buffer_len;
|
||||||
|
let mut best_score = 0;
|
||||||
|
for col in 1..=buffer_len {
|
||||||
|
let score = matrix.get(query_len, col).score;
|
||||||
|
if score > best_score {
|
||||||
|
best_score = score;
|
||||||
|
best_buffer_end = col;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut query_ix = query_len;
|
||||||
|
let mut buffer_ix = best_buffer_end;
|
||||||
|
while query_ix > 0 && buffer_ix > 0 {
|
||||||
|
let current = matrix.get(query_ix, buffer_ix);
|
||||||
|
match current.direction {
|
||||||
|
SearchDirection::Diagonal => {
|
||||||
|
query_ix -= 1;
|
||||||
|
buffer_ix -= 1;
|
||||||
|
}
|
||||||
|
SearchDirection::Up => {
|
||||||
|
query_ix -= 1;
|
||||||
|
}
|
||||||
|
SearchDirection::Left => {
|
||||||
|
buffer_ix -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut start = buffer.offset_to_point(buffer.clip_offset(buffer_ix, Bias::Left));
|
||||||
|
start.column = 0;
|
||||||
|
let mut end = buffer.offset_to_point(buffer.clip_offset(best_buffer_end, Bias::Right));
|
||||||
|
if end.column > 0 {
|
||||||
|
end.column = buffer.line_len(end.row);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.anchor_after(start)..buffer.anchor_before(end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssistantPatch {
|
||||||
|
pub(crate) async fn resolve(
|
||||||
|
&self,
|
||||||
|
project: Model<Project>,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> ResolvedPatch {
|
||||||
|
let mut resolve_tasks = Vec::new();
|
||||||
|
for (ix, edit) in self.edits.iter().enumerate() {
|
||||||
|
if let Ok(edit) = edit.as_ref() {
|
||||||
|
resolve_tasks.push(
|
||||||
|
edit.resolve(project.clone(), cx.clone())
|
||||||
|
.map_err(move |error| (ix, error)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let edits = future::join_all(resolve_tasks).await;
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
let mut edits_by_buffer = HashMap::default();
|
||||||
|
for entry in edits {
|
||||||
|
match entry {
|
||||||
|
Ok((buffer, edit)) => {
|
||||||
|
edits_by_buffer
|
||||||
|
.entry(buffer)
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(edit);
|
||||||
|
}
|
||||||
|
Err((edit_ix, error)) => errors.push(AssistantPatchResolutionError {
|
||||||
|
edit_ix,
|
||||||
|
message: error.to_string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand the context ranges of each edit and group edits with overlapping context ranges.
|
||||||
|
let mut edit_groups_by_buffer = HashMap::default();
|
||||||
|
for (buffer, edits) in edits_by_buffer {
|
||||||
|
if let Ok(snapshot) = buffer.update(cx, |buffer, _| buffer.text_snapshot()) {
|
||||||
|
edit_groups_by_buffer.insert(buffer, Self::group_edits(edits, &snapshot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ResolvedPatch {
|
||||||
|
edit_groups: edit_groups_by_buffer,
|
||||||
|
errors,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn group_edits(
|
||||||
|
mut edits: Vec<ResolvedEdit>,
|
||||||
|
snapshot: &text::BufferSnapshot,
|
||||||
|
) -> Vec<ResolvedEditGroup> {
|
||||||
|
let mut edit_groups = Vec::<ResolvedEditGroup>::new();
|
||||||
|
// Sort edits by their range so that earlier, larger ranges come first
|
||||||
|
edits.sort_by(|a, b| a.range.cmp(&b.range, &snapshot));
|
||||||
|
|
||||||
|
// Merge overlapping edits
|
||||||
|
edits.dedup_by(|a, b| b.try_merge(a, &snapshot));
|
||||||
|
|
||||||
|
// Create context ranges for each edit
|
||||||
|
for edit in edits {
|
||||||
|
let context_range = {
|
||||||
|
let edit_point_range = edit.range.to_point(&snapshot);
|
||||||
|
let start_row = edit_point_range.start.row.saturating_sub(5);
|
||||||
|
let end_row = cmp::min(edit_point_range.end.row + 5, snapshot.max_point().row);
|
||||||
|
let start = snapshot.anchor_before(Point::new(start_row, 0));
|
||||||
|
let end = snapshot.anchor_after(Point::new(end_row, snapshot.line_len(end_row)));
|
||||||
|
start..end
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(last_group) = edit_groups.last_mut() {
|
||||||
|
if last_group
|
||||||
|
.context_range
|
||||||
|
.end
|
||||||
|
.cmp(&context_range.start, &snapshot)
|
||||||
|
.is_ge()
|
||||||
|
{
|
||||||
|
// Merge with the previous group if context ranges overlap
|
||||||
|
last_group.context_range.end = context_range.end;
|
||||||
|
last_group.edits.push(edit);
|
||||||
|
} else {
|
||||||
|
// Create a new group
|
||||||
|
edit_groups.push(ResolvedEditGroup {
|
||||||
|
context_range,
|
||||||
|
edits: vec![edit],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create the first group
|
||||||
|
edit_groups.push(ResolvedEditGroup {
|
||||||
|
context_range,
|
||||||
|
edits: vec![edit],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
edit_groups
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path_count(&self) -> usize {
|
||||||
|
self.paths().count()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paths(&self) -> impl '_ + Iterator<Item = &str> {
|
||||||
|
let mut prev_path = None;
|
||||||
|
self.edits.iter().filter_map(move |edit| {
|
||||||
|
if let Ok(edit) = edit {
|
||||||
|
let path = Some(edit.path.as_str());
|
||||||
|
if path != prev_path {
|
||||||
|
prev_path = path;
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for AssistantPatch {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.range == other.range
|
||||||
|
&& self.title == other.title
|
||||||
|
&& Arc::ptr_eq(&self.edits, &other.edits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for AssistantPatch {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use gpui::{AppContext, Context};
|
||||||
|
use language::{
|
||||||
|
language_settings::AllLanguageSettings, Language, LanguageConfig, LanguageMatcher,
|
||||||
|
};
|
||||||
|
use settings::SettingsStore;
|
||||||
|
use text::{OffsetRangeExt, Point};
|
||||||
|
use ui::BorrowAppContext;
|
||||||
|
use unindent::Unindent as _;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_resolve_location(cx: &mut AppContext) {
|
||||||
|
{
|
||||||
|
let buffer = cx.new_model(|cx| {
|
||||||
|
Buffer::local(
|
||||||
|
concat!(
|
||||||
|
" Lorem\n",
|
||||||
|
" ipsum\n",
|
||||||
|
" dolor sit amet\n",
|
||||||
|
" consecteur",
|
||||||
|
),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
|
assert_eq!(
|
||||||
|
AssistantEditKind::resolve_location(&snapshot, "ipsum\ndolor").to_point(&snapshot),
|
||||||
|
Point::new(1, 0)..Point::new(2, 18)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let buffer = cx.new_model(|cx| {
|
||||||
|
Buffer::local(
|
||||||
|
concat!(
|
||||||
|
"fn foo1(a: usize) -> usize {\n",
|
||||||
|
" 40\n",
|
||||||
|
"}\n",
|
||||||
|
"\n",
|
||||||
|
"fn foo2(b: usize) -> usize {\n",
|
||||||
|
" 42\n",
|
||||||
|
"}\n",
|
||||||
|
),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
|
assert_eq!(
|
||||||
|
AssistantEditKind::resolve_location(&snapshot, "fn foo1(b: usize) {\n40\n}")
|
||||||
|
.to_point(&snapshot),
|
||||||
|
Point::new(0, 0)..Point::new(2, 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let buffer = cx.new_model(|cx| {
|
||||||
|
Buffer::local(
|
||||||
|
concat!(
|
||||||
|
"fn main() {\n",
|
||||||
|
" Foo\n",
|
||||||
|
" .bar()\n",
|
||||||
|
" .baz()\n",
|
||||||
|
" .qux()\n",
|
||||||
|
"}\n",
|
||||||
|
"\n",
|
||||||
|
"fn foo2(b: usize) -> usize {\n",
|
||||||
|
" 42\n",
|
||||||
|
"}\n",
|
||||||
|
),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
|
assert_eq!(
|
||||||
|
AssistantEditKind::resolve_location(&snapshot, "Foo.bar.baz.qux()")
|
||||||
|
.to_point(&snapshot),
|
||||||
|
Point::new(1, 0)..Point::new(4, 14)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_resolve_edits(cx: &mut AppContext) {
|
||||||
|
let settings_store = SettingsStore::test(cx);
|
||||||
|
cx.set_global(settings_store);
|
||||||
|
language::init(cx);
|
||||||
|
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
||||||
|
settings.update_user_settings::<AllLanguageSettings>(cx, |_| {});
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_edits(
|
||||||
|
"
|
||||||
|
/// A person
|
||||||
|
struct Person {
|
||||||
|
name: String,
|
||||||
|
age: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A dog
|
||||||
|
struct Dog {
|
||||||
|
weight: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Person {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"
|
||||||
|
.unindent(),
|
||||||
|
vec![
|
||||||
|
AssistantEditKind::Update {
|
||||||
|
old_text: "
|
||||||
|
name: String,
|
||||||
|
"
|
||||||
|
.unindent(),
|
||||||
|
new_text: "
|
||||||
|
first_name: String,
|
||||||
|
last_name: String,
|
||||||
|
"
|
||||||
|
.unindent(),
|
||||||
|
description: "".into(),
|
||||||
|
},
|
||||||
|
AssistantEditKind::Update {
|
||||||
|
old_text: "
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
"
|
||||||
|
.unindent(),
|
||||||
|
new_text: "
|
||||||
|
fn name(&self) -> String {
|
||||||
|
format!(\"{} {}\", self.first_name, self.last_name)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
.unindent(),
|
||||||
|
description: "".into(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"
|
||||||
|
/// A person
|
||||||
|
struct Person {
|
||||||
|
first_name: String,
|
||||||
|
last_name: String,
|
||||||
|
age: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A dog
|
||||||
|
struct Dog {
|
||||||
|
weight: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Person {
|
||||||
|
fn name(&self) -> String {
|
||||||
|
format!(\"{} {}\", self.first_name, self.last_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"
|
||||||
|
.unindent(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn assert_edits(
|
||||||
|
old_text: String,
|
||||||
|
edits: Vec<AssistantEditKind>,
|
||||||
|
new_text: String,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) {
|
||||||
|
let buffer =
|
||||||
|
cx.new_model(|cx| Buffer::local(old_text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||||
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
|
let resolved_edits = edits
|
||||||
|
.into_iter()
|
||||||
|
.map(|kind| kind.resolve(&snapshot))
|
||||||
|
.collect();
|
||||||
|
let edit_groups = AssistantPatch::group_edits(resolved_edits, &snapshot);
|
||||||
|
ResolvedPatch::apply_edit_groups(&edit_groups, &buffer, cx);
|
||||||
|
let actual_new_text = buffer.read(cx).text();
|
||||||
|
pretty_assertions::assert_eq!(actual_new_text, new_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rust_lang() -> Language {
|
||||||
|
Language::new(
|
||||||
|
LanguageConfig {
|
||||||
|
name: "Rust".into(),
|
||||||
|
matcher: LanguageMatcher {
|
||||||
|
path_suffixes: vec!["rs".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some(language::tree_sitter_rust::LANGUAGE.into()),
|
||||||
|
)
|
||||||
|
.with_indents_query(
|
||||||
|
r#"
|
||||||
|
(call_expression) @indent
|
||||||
|
(field_expression) @indent
|
||||||
|
(_ "(" ")" @end) @indent
|
||||||
|
(_ "{" "}" @end) @indent
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
|
@ -45,15 +45,6 @@ pub struct ProjectSlashCommandPromptContext {
|
||||||
pub context_buffer: String,
|
pub context_buffer: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Context required to generate a workflow step resolution prompt.
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub struct StepResolutionContext {
|
|
||||||
/// The full context, including <step>...</step> tags
|
|
||||||
pub workflow_context: String,
|
|
||||||
/// The text of the specific step from the context to resolve
|
|
||||||
pub step_to_resolve: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PromptLoadingParams<'a> {
|
pub struct PromptLoadingParams<'a> {
|
||||||
pub fs: Arc<dyn Fs>,
|
pub fs: Arc<dyn Fs>,
|
||||||
pub repo_path: Option<PathBuf>,
|
pub repo_path: Option<PathBuf>,
|
||||||
|
|
|
@ -18,6 +18,8 @@ pub(crate) struct WorkflowSlashCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WorkflowSlashCommand {
|
impl WorkflowSlashCommand {
|
||||||
|
pub const NAME: &'static str = "workflow";
|
||||||
|
|
||||||
pub fn new(prompt_builder: Arc<PromptBuilder>) -> Self {
|
pub fn new(prompt_builder: Arc<PromptBuilder>) -> Self {
|
||||||
Self { prompt_builder }
|
Self { prompt_builder }
|
||||||
}
|
}
|
||||||
|
@ -25,7 +27,7 @@ impl WorkflowSlashCommand {
|
||||||
|
|
||||||
impl SlashCommand for WorkflowSlashCommand {
|
impl SlashCommand for WorkflowSlashCommand {
|
||||||
fn name(&self) -> String {
|
fn name(&self) -> String {
|
||||||
"workflow".into()
|
Self::NAME.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn description(&self) -> String {
|
fn description(&self) -> String {
|
||||||
|
|
|
@ -1,507 +0,0 @@
|
||||||
use crate::{AssistantPanel, InlineAssistId, InlineAssistant};
|
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
|
||||||
use collections::HashMap;
|
|
||||||
use editor::Editor;
|
|
||||||
use gpui::AsyncAppContext;
|
|
||||||
use gpui::{Model, Task, UpdateGlobal as _, View, WeakView, WindowContext};
|
|
||||||
use language::{Buffer, BufferSnapshot};
|
|
||||||
use project::{Project, ProjectPath};
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::{ops::Range, path::Path, sync::Arc};
|
|
||||||
use text::Bias;
|
|
||||||
use workspace::Workspace;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub(crate) struct WorkflowStep {
|
|
||||||
pub range: Range<language::Anchor>,
|
|
||||||
pub leading_tags_end: text::Anchor,
|
|
||||||
pub trailing_tag_start: Option<text::Anchor>,
|
|
||||||
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)]
|
|
||||||
pub(crate) struct WorkflowStepResolution {
|
|
||||||
pub title: String,
|
|
||||||
pub suggestion_groups: HashMap<Model<Buffer>, Vec<WorkflowSuggestionGroup>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub struct WorkflowSuggestionGroup {
|
|
||||||
pub context_range: Range<language::Anchor>,
|
|
||||||
pub suggestions: Vec<WorkflowSuggestion>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub enum WorkflowSuggestion {
|
|
||||||
Update {
|
|
||||||
range: Range<language::Anchor>,
|
|
||||||
description: String,
|
|
||||||
},
|
|
||||||
CreateFile {
|
|
||||||
description: String,
|
|
||||||
},
|
|
||||||
InsertBefore {
|
|
||||||
position: language::Anchor,
|
|
||||||
description: String,
|
|
||||||
},
|
|
||||||
InsertAfter {
|
|
||||||
position: language::Anchor,
|
|
||||||
description: String,
|
|
||||||
},
|
|
||||||
Delete {
|
|
||||||
range: Range<language::Anchor>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WorkflowSuggestion {
|
|
||||||
pub fn range(&self) -> Range<language::Anchor> {
|
|
||||||
match self {
|
|
||||||
Self::Update { range, .. } => range.clone(),
|
|
||||||
Self::CreateFile { .. } => language::Anchor::MIN..language::Anchor::MAX,
|
|
||||||
Self::InsertBefore { position, .. } | Self::InsertAfter { position, .. } => {
|
|
||||||
*position..*position
|
|
||||||
}
|
|
||||||
Self::Delete { range, .. } => range.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn description(&self) -> Option<&str> {
|
|
||||||
match self {
|
|
||||||
Self::Update { description, .. }
|
|
||||||
| Self::CreateFile { description }
|
|
||||||
| Self::InsertBefore { description, .. }
|
|
||||||
| Self::InsertAfter { description, .. } => Some(description),
|
|
||||||
Self::Delete { .. } => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description_mut(&mut self) -> Option<&mut String> {
|
|
||||||
match self {
|
|
||||||
Self::Update { description, .. }
|
|
||||||
| Self::CreateFile { description }
|
|
||||||
| Self::InsertBefore { description, .. }
|
|
||||||
| Self::InsertAfter { description, .. } => Some(description),
|
|
||||||
Self::Delete { .. } => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn try_merge(&mut self, other: &Self, buffer: &BufferSnapshot) -> bool {
|
|
||||||
let range = self.range();
|
|
||||||
let other_range = other.range();
|
|
||||||
|
|
||||||
// Don't merge if we don't contain the other suggestion.
|
|
||||||
if range.start.cmp(&other_range.start, buffer).is_gt()
|
|
||||||
|| range.end.cmp(&other_range.end, buffer).is_lt()
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(description) = self.description_mut() {
|
|
||||||
if let Some(other_description) = other.description() {
|
|
||||||
description.push('\n');
|
|
||||||
description.push_str(other_description);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn show(
|
|
||||||
&self,
|
|
||||||
editor: &View<Editor>,
|
|
||||||
excerpt_id: editor::ExcerptId,
|
|
||||||
workspace: &WeakView<Workspace>,
|
|
||||||
assistant_panel: &View<AssistantPanel>,
|
|
||||||
cx: &mut WindowContext,
|
|
||||||
) -> Option<InlineAssistId> {
|
|
||||||
let mut initial_transaction_id = None;
|
|
||||||
let initial_prompt;
|
|
||||||
let suggestion_range;
|
|
||||||
let buffer = editor.read(cx).buffer().clone();
|
|
||||||
let snapshot = buffer.read(cx).snapshot(cx);
|
|
||||||
|
|
||||||
match self {
|
|
||||||
Self::Update {
|
|
||||||
range, description, ..
|
|
||||||
} => {
|
|
||||||
initial_prompt = description.clone();
|
|
||||||
suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
|
|
||||||
..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
|
|
||||||
}
|
|
||||||
Self::CreateFile { description } => {
|
|
||||||
initial_prompt = description.clone();
|
|
||||||
suggestion_range = editor::Anchor::min()..editor::Anchor::min();
|
|
||||||
}
|
|
||||||
Self::InsertBefore {
|
|
||||||
position,
|
|
||||||
description,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
|
|
||||||
initial_prompt = description.clone();
|
|
||||||
suggestion_range = buffer.update(cx, |buffer, cx| {
|
|
||||||
buffer.start_transaction(cx);
|
|
||||||
let line_start = buffer.insert_empty_line(position, true, true, cx);
|
|
||||||
initial_transaction_id = buffer.end_transaction(cx);
|
|
||||||
buffer.refresh_preview(cx);
|
|
||||||
|
|
||||||
let line_start = buffer.read(cx).anchor_before(line_start);
|
|
||||||
line_start..line_start
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Self::InsertAfter {
|
|
||||||
position,
|
|
||||||
description,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
|
|
||||||
initial_prompt = description.clone();
|
|
||||||
suggestion_range = buffer.update(cx, |buffer, cx| {
|
|
||||||
buffer.start_transaction(cx);
|
|
||||||
let line_start = buffer.insert_empty_line(position, true, true, cx);
|
|
||||||
initial_transaction_id = buffer.end_transaction(cx);
|
|
||||||
buffer.refresh_preview(cx);
|
|
||||||
|
|
||||||
let line_start = buffer.read(cx).anchor_before(line_start);
|
|
||||||
line_start..line_start
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Self::Delete { range, .. } => {
|
|
||||||
initial_prompt = "Delete".to_string();
|
|
||||||
suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
|
|
||||||
..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
InlineAssistant::update_global(cx, |inline_assistant, cx| {
|
|
||||||
Some(inline_assistant.suggest_assist(
|
|
||||||
editor,
|
|
||||||
suggestion_range,
|
|
||||||
initial_prompt,
|
|
||||||
initial_transaction_id,
|
|
||||||
false,
|
|
||||||
Some(workspace.clone()),
|
|
||||||
Some(assistant_panel),
|
|
||||||
cx,
|
|
||||||
))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WorkflowStepEdit {
|
|
||||||
pub fn new(
|
|
||||||
path: Option<String>,
|
|
||||||
operation: Option<String>,
|
|
||||||
search: Option<String>,
|
|
||||||
description: Option<String>,
|
|
||||||
) -> Result<Self> {
|
|
||||||
let path = path.ok_or_else(|| anyhow!("missing path"))?;
|
|
||||||
let operation = operation.ok_or_else(|| anyhow!("missing operation"))?;
|
|
||||||
|
|
||||||
let kind = match operation.as_str() {
|
|
||||||
"update" => WorkflowStepEditKind::Update {
|
|
||||||
search: search.ok_or_else(|| anyhow!("missing search"))?,
|
|
||||||
description: description.ok_or_else(|| anyhow!("missing description"))?,
|
|
||||||
},
|
|
||||||
"insert_before" => WorkflowStepEditKind::InsertBefore {
|
|
||||||
search: search.ok_or_else(|| anyhow!("missing search"))?,
|
|
||||||
description: description.ok_or_else(|| anyhow!("missing description"))?,
|
|
||||||
},
|
|
||||||
"insert_after" => WorkflowStepEditKind::InsertAfter {
|
|
||||||
search: search.ok_or_else(|| anyhow!("missing search"))?,
|
|
||||||
description: description.ok_or_else(|| anyhow!("missing description"))?,
|
|
||||||
},
|
|
||||||
"delete" => WorkflowStepEditKind::Delete {
|
|
||||||
search: search.ok_or_else(|| anyhow!("missing search"))?,
|
|
||||||
},
|
|
||||||
"create" => WorkflowStepEditKind::Create {
|
|
||||||
description: description.ok_or_else(|| anyhow!("missing description"))?,
|
|
||||||
},
|
|
||||||
_ => Err(anyhow!("unknown operation {operation:?}"))?,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self { path, kind })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn resolve(
|
|
||||||
&self,
|
|
||||||
project: Model<Project>,
|
|
||||||
mut cx: AsyncAppContext,
|
|
||||||
) -> Result<(Model<Buffer>, super::WorkflowSuggestion)> {
|
|
||||||
let path = self.path.clone();
|
|
||||||
let kind = self.kind.clone();
|
|
||||||
let buffer = project
|
|
||||||
.update(&mut cx, |project, cx| {
|
|
||||||
let project_path = project
|
|
||||||
.find_project_path(Path::new(&path), cx)
|
|
||||||
.or_else(|| {
|
|
||||||
// If we couldn't find a project path for it, put it in the active worktree
|
|
||||||
// so that when we create the buffer, it can be saved.
|
|
||||||
let worktree = project
|
|
||||||
.active_entry()
|
|
||||||
.and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
|
|
||||||
.or_else(|| project.worktrees(cx).next())?;
|
|
||||||
let worktree = worktree.read(cx);
|
|
||||||
|
|
||||||
Some(ProjectPath {
|
|
||||||
worktree_id: worktree.id(),
|
|
||||||
path: Arc::from(Path::new(&path)),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.with_context(|| format!("worktree not found for {:?}", path))?;
|
|
||||||
anyhow::Ok(project.open_buffer(project_path, cx))
|
|
||||||
})??
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
|
|
||||||
let suggestion = cx
|
|
||||||
.background_executor()
|
|
||||||
.spawn(async move {
|
|
||||||
match kind {
|
|
||||||
WorkflowStepEditKind::Update {
|
|
||||||
search,
|
|
||||||
description,
|
|
||||||
} => {
|
|
||||||
let range = Self::resolve_location(&snapshot, &search);
|
|
||||||
WorkflowSuggestion::Update { range, description }
|
|
||||||
}
|
|
||||||
WorkflowStepEditKind::Create { description } => {
|
|
||||||
WorkflowSuggestion::CreateFile { description }
|
|
||||||
}
|
|
||||||
WorkflowStepEditKind::InsertBefore {
|
|
||||||
search,
|
|
||||||
description,
|
|
||||||
} => {
|
|
||||||
let range = Self::resolve_location(&snapshot, &search);
|
|
||||||
WorkflowSuggestion::InsertBefore {
|
|
||||||
position: range.start,
|
|
||||||
description,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WorkflowStepEditKind::InsertAfter {
|
|
||||||
search,
|
|
||||||
description,
|
|
||||||
} => {
|
|
||||||
let range = Self::resolve_location(&snapshot, &search);
|
|
||||||
WorkflowSuggestion::InsertAfter {
|
|
||||||
position: range.end,
|
|
||||||
description,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WorkflowStepEditKind::Delete { search } => {
|
|
||||||
let range = Self::resolve_location(&snapshot, &search);
|
|
||||||
WorkflowSuggestion::Delete { range }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok((buffer, suggestion))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_location(buffer: &text::BufferSnapshot, search_query: &str) -> Range<text::Anchor> {
|
|
||||||
const INSERTION_SCORE: f64 = -1.0;
|
|
||||||
const DELETION_SCORE: f64 = -1.0;
|
|
||||||
const REPLACEMENT_SCORE: f64 = -1.0;
|
|
||||||
const EQUALITY_SCORE: f64 = 5.0;
|
|
||||||
|
|
||||||
struct Matrix {
|
|
||||||
cols: usize,
|
|
||||||
data: Vec<f64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Matrix {
|
|
||||||
fn new(rows: usize, cols: usize) -> Self {
|
|
||||||
Matrix {
|
|
||||||
cols,
|
|
||||||
data: vec![0.0; rows * cols],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get(&self, row: usize, col: usize) -> f64 {
|
|
||||||
self.data[row * self.cols + col]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set(&mut self, row: usize, col: usize, value: f64) {
|
|
||||||
self.data[row * self.cols + col] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let buffer_len = buffer.len();
|
|
||||||
let query_len = search_query.len();
|
|
||||||
let mut matrix = Matrix::new(query_len + 1, buffer_len + 1);
|
|
||||||
|
|
||||||
for (i, query_byte) in search_query.bytes().enumerate() {
|
|
||||||
for (j, buffer_byte) in buffer.bytes_in_range(0..buffer.len()).flatten().enumerate() {
|
|
||||||
let match_score = if query_byte == *buffer_byte {
|
|
||||||
EQUALITY_SCORE
|
|
||||||
} else {
|
|
||||||
REPLACEMENT_SCORE
|
|
||||||
};
|
|
||||||
let up = matrix.get(i + 1, j) + DELETION_SCORE;
|
|
||||||
let left = matrix.get(i, j + 1) + INSERTION_SCORE;
|
|
||||||
let diagonal = matrix.get(i, j) + match_score;
|
|
||||||
let score = up.max(left.max(diagonal)).max(0.);
|
|
||||||
matrix.set(i + 1, j + 1, score);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Traceback to find the best match
|
|
||||||
let mut best_buffer_end = buffer_len;
|
|
||||||
let mut best_score = 0.0;
|
|
||||||
for col in 1..=buffer_len {
|
|
||||||
let score = matrix.get(query_len, col);
|
|
||||||
if score > best_score {
|
|
||||||
best_score = score;
|
|
||||||
best_buffer_end = col;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut query_ix = query_len;
|
|
||||||
let mut buffer_ix = best_buffer_end;
|
|
||||||
while query_ix > 0 && buffer_ix > 0 {
|
|
||||||
let current = matrix.get(query_ix, buffer_ix);
|
|
||||||
let up = matrix.get(query_ix - 1, buffer_ix);
|
|
||||||
let left = matrix.get(query_ix, buffer_ix - 1);
|
|
||||||
if current == left + INSERTION_SCORE {
|
|
||||||
buffer_ix -= 1;
|
|
||||||
} else if current == up + DELETION_SCORE {
|
|
||||||
query_ix -= 1;
|
|
||||||
} else {
|
|
||||||
query_ix -= 1;
|
|
||||||
buffer_ix -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut start = buffer.offset_to_point(buffer.clip_offset(buffer_ix, Bias::Left));
|
|
||||||
start.column = 0;
|
|
||||||
let mut end = buffer.offset_to_point(buffer.clip_offset(best_buffer_end, Bias::Right));
|
|
||||||
end.column = buffer.line_len(end.row);
|
|
||||||
|
|
||||||
buffer.anchor_after(start)..buffer.anchor_before(end)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
|
||||||
#[serde(tag = "operation")]
|
|
||||||
pub enum WorkflowStepEditKind {
|
|
||||||
/// Rewrites the specified text entirely based on the given description.
|
|
||||||
/// This operation completely replaces the given text.
|
|
||||||
Update {
|
|
||||||
/// A string in the source text to apply the update to.
|
|
||||||
search: String,
|
|
||||||
/// A brief description of the transformation to apply to the symbol.
|
|
||||||
description: String,
|
|
||||||
},
|
|
||||||
/// Creates a new file with the given path based on the provided description.
|
|
||||||
/// This operation adds a new file to the codebase.
|
|
||||||
Create {
|
|
||||||
/// A brief description of the file to be created.
|
|
||||||
description: String,
|
|
||||||
},
|
|
||||||
/// Inserts text before the specified text in the source file.
|
|
||||||
InsertBefore {
|
|
||||||
/// A string in the source text to insert text before.
|
|
||||||
search: String,
|
|
||||||
/// A brief description of how the new text should be generated.
|
|
||||||
description: String,
|
|
||||||
},
|
|
||||||
/// Inserts text after the specified text in the source file.
|
|
||||||
InsertAfter {
|
|
||||||
/// A string in the source text to insert text after.
|
|
||||||
search: String,
|
|
||||||
/// A brief description of how the new text should be generated.
|
|
||||||
description: String,
|
|
||||||
},
|
|
||||||
/// Deletes the specified symbol from the containing file.
|
|
||||||
Delete {
|
|
||||||
/// A string in the source text to delete.
|
|
||||||
search: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use gpui::{AppContext, Context};
|
|
||||||
use text::{OffsetRangeExt, Point};
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
fn test_resolve_location(cx: &mut AppContext) {
|
|
||||||
{
|
|
||||||
let buffer = cx.new_model(|cx| {
|
|
||||||
Buffer::local(
|
|
||||||
concat!(
|
|
||||||
" Lorem\n",
|
|
||||||
" ipsum\n",
|
|
||||||
" dolor sit amet\n",
|
|
||||||
" consecteur",
|
|
||||||
),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let snapshot = buffer.read(cx).snapshot();
|
|
||||||
assert_eq!(
|
|
||||||
WorkflowStepEdit::resolve_location(&snapshot, "ipsum\ndolor").to_point(&snapshot),
|
|
||||||
Point::new(1, 0)..Point::new(2, 18)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let buffer = cx.new_model(|cx| {
|
|
||||||
Buffer::local(
|
|
||||||
concat!(
|
|
||||||
"fn foo1(a: usize) -> usize {\n",
|
|
||||||
" 42\n",
|
|
||||||
"}\n",
|
|
||||||
"\n",
|
|
||||||
"fn foo2(b: usize) -> usize {\n",
|
|
||||||
" 42\n",
|
|
||||||
"}\n",
|
|
||||||
),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let snapshot = buffer.read(cx).snapshot();
|
|
||||||
assert_eq!(
|
|
||||||
WorkflowStepEdit::resolve_location(&snapshot, "fn foo1(b: usize) {\n42\n}")
|
|
||||||
.to_point(&snapshot),
|
|
||||||
Point::new(0, 0)..Point::new(2, 1)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let buffer = cx.new_model(|cx| {
|
|
||||||
Buffer::local(
|
|
||||||
concat!(
|
|
||||||
"fn main() {\n",
|
|
||||||
" Foo\n",
|
|
||||||
" .bar()\n",
|
|
||||||
" .baz()\n",
|
|
||||||
" .qux()\n",
|
|
||||||
"}\n",
|
|
||||||
"\n",
|
|
||||||
"fn foo2(b: usize) -> usize {\n",
|
|
||||||
" 42\n",
|
|
||||||
"}\n",
|
|
||||||
),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let snapshot = buffer.read(cx).snapshot();
|
|
||||||
assert_eq!(
|
|
||||||
WorkflowStepEdit::resolve_location(&snapshot, "Foo.bar.baz.qux()")
|
|
||||||
.to_point(&snapshot),
|
|
||||||
Point::new(1, 0)..Point::new(4, 14)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -100,7 +100,7 @@ use language::{
|
||||||
};
|
};
|
||||||
use linked_editing_ranges::refresh_linked_ranges;
|
use linked_editing_ranges::refresh_linked_ranges;
|
||||||
pub use proposed_changes_editor::{
|
pub use proposed_changes_editor::{
|
||||||
ProposedChangesBuffer, ProposedChangesEditor, ProposedChangesEditorToolbar,
|
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
|
||||||
};
|
};
|
||||||
use similar::{ChangeTag, TextDiff};
|
use similar::{ChangeTag, TextDiff};
|
||||||
use task::{ResolvedTask, TaskTemplate, TaskVariables};
|
use task::{ResolvedTask, TaskTemplate, TaskVariables};
|
||||||
|
@ -12363,10 +12363,15 @@ impl Editor {
|
||||||
|
|
||||||
let proposed_changes_buffers = new_selections_by_buffer
|
let proposed_changes_buffers = new_selections_by_buffer
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(buffer, ranges)| ProposedChangesBuffer { buffer, ranges })
|
.map(|(buffer, ranges)| ProposedChangeLocation { buffer, ranges })
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let proposed_changes_editor = cx.new_view(|cx| {
|
let proposed_changes_editor = cx.new_view(|cx| {
|
||||||
ProposedChangesEditor::new(proposed_changes_buffers, self.project.clone(), cx)
|
ProposedChangesEditor::new(
|
||||||
|
"Proposed changes",
|
||||||
|
proposed_changes_buffers,
|
||||||
|
self.project.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.window_context().defer(move |cx| {
|
cx.window_context().defer(move |cx| {
|
||||||
|
|
|
@ -16,16 +16,24 @@ use workspace::{
|
||||||
|
|
||||||
pub struct ProposedChangesEditor {
|
pub struct ProposedChangesEditor {
|
||||||
editor: View<Editor>,
|
editor: View<Editor>,
|
||||||
_subscriptions: Vec<Subscription>,
|
multibuffer: Model<MultiBuffer>,
|
||||||
|
title: SharedString,
|
||||||
|
buffer_entries: Vec<BufferEntry>,
|
||||||
_recalculate_diffs_task: Task<Option<()>>,
|
_recalculate_diffs_task: Task<Option<()>>,
|
||||||
recalculate_diffs_tx: mpsc::UnboundedSender<RecalculateDiff>,
|
recalculate_diffs_tx: mpsc::UnboundedSender<RecalculateDiff>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ProposedChangesBuffer<T> {
|
pub struct ProposedChangeLocation<T> {
|
||||||
pub buffer: Model<Buffer>,
|
pub buffer: Model<Buffer>,
|
||||||
pub ranges: Vec<Range<T>>,
|
pub ranges: Vec<Range<T>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct BufferEntry {
|
||||||
|
base: Model<Buffer>,
|
||||||
|
branch: Model<Buffer>,
|
||||||
|
_subscription: Subscription,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ProposedChangesEditorToolbar {
|
pub struct ProposedChangesEditorToolbar {
|
||||||
current_editor: Option<View<ProposedChangesEditor>>,
|
current_editor: Option<View<ProposedChangesEditor>>,
|
||||||
}
|
}
|
||||||
|
@ -43,32 +51,14 @@ struct BranchBufferSemanticsProvider(Rc<dyn SemanticsProvider>);
|
||||||
|
|
||||||
impl ProposedChangesEditor {
|
impl ProposedChangesEditor {
|
||||||
pub fn new<T: ToOffset>(
|
pub fn new<T: ToOffset>(
|
||||||
buffers: Vec<ProposedChangesBuffer<T>>,
|
title: impl Into<SharedString>,
|
||||||
|
locations: Vec<ProposedChangeLocation<T>>,
|
||||||
project: Option<Model<Project>>,
|
project: Option<Model<Project>>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut subscriptions = Vec::new();
|
|
||||||
let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite));
|
let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite));
|
||||||
|
|
||||||
for buffer in buffers {
|
|
||||||
let branch_buffer = buffer.buffer.update(cx, |buffer, cx| buffer.branch(cx));
|
|
||||||
subscriptions.push(cx.subscribe(&branch_buffer, Self::on_buffer_event));
|
|
||||||
|
|
||||||
multibuffer.update(cx, |multibuffer, cx| {
|
|
||||||
multibuffer.push_excerpts(
|
|
||||||
branch_buffer,
|
|
||||||
buffer.ranges.into_iter().map(|range| ExcerptRange {
|
|
||||||
context: range,
|
|
||||||
primary: None,
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let (recalculate_diffs_tx, mut recalculate_diffs_rx) = mpsc::unbounded();
|
let (recalculate_diffs_tx, mut recalculate_diffs_rx) = mpsc::unbounded();
|
||||||
|
let mut this = Self {
|
||||||
Self {
|
|
||||||
editor: cx.new_view(|cx| {
|
editor: cx.new_view(|cx| {
|
||||||
let mut editor = Editor::for_multibuffer(multibuffer.clone(), project, true, cx);
|
let mut editor = Editor::for_multibuffer(multibuffer.clone(), project, true, cx);
|
||||||
editor.set_expand_all_diff_hunks();
|
editor.set_expand_all_diff_hunks();
|
||||||
|
@ -81,6 +71,9 @@ impl ProposedChangesEditor {
|
||||||
);
|
);
|
||||||
editor
|
editor
|
||||||
}),
|
}),
|
||||||
|
multibuffer,
|
||||||
|
title: title.into(),
|
||||||
|
buffer_entries: Vec::new(),
|
||||||
recalculate_diffs_tx,
|
recalculate_diffs_tx,
|
||||||
_recalculate_diffs_task: cx.spawn(|_, mut cx| async move {
|
_recalculate_diffs_task: cx.spawn(|_, mut cx| async move {
|
||||||
let mut buffers_to_diff = HashSet::default();
|
let mut buffers_to_diff = HashSet::default();
|
||||||
|
@ -112,7 +105,100 @@ impl ProposedChangesEditor {
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}),
|
}),
|
||||||
_subscriptions: subscriptions,
|
};
|
||||||
|
this.reset_locations(locations, cx);
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn branch_buffer_for_base(&self, base_buffer: &Model<Buffer>) -> Option<Model<Buffer>> {
|
||||||
|
self.buffer_entries.iter().find_map(|entry| {
|
||||||
|
if &entry.base == base_buffer {
|
||||||
|
Some(entry.branch.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_title(&mut self, title: SharedString, cx: &mut ViewContext<Self>) {
|
||||||
|
self.title = title;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_locations<T: ToOffset>(
|
||||||
|
&mut self,
|
||||||
|
locations: Vec<ProposedChangeLocation<T>>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
// Undo all branch changes
|
||||||
|
for entry in &self.buffer_entries {
|
||||||
|
let base_version = entry.base.read(cx).version();
|
||||||
|
entry.branch.update(cx, |buffer, cx| {
|
||||||
|
let undo_counts = buffer
|
||||||
|
.operations()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(timestamp, _)| {
|
||||||
|
if !base_version.observed(*timestamp) {
|
||||||
|
Some((*timestamp, u32::MAX))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
buffer.undo_operations(undo_counts, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.multibuffer.update(cx, |multibuffer, cx| {
|
||||||
|
multibuffer.clear(cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut buffer_entries = Vec::new();
|
||||||
|
for location in locations {
|
||||||
|
let branch_buffer;
|
||||||
|
if let Some(ix) = self
|
||||||
|
.buffer_entries
|
||||||
|
.iter()
|
||||||
|
.position(|entry| entry.base == location.buffer)
|
||||||
|
{
|
||||||
|
let entry = self.buffer_entries.remove(ix);
|
||||||
|
branch_buffer = entry.branch.clone();
|
||||||
|
buffer_entries.push(entry);
|
||||||
|
} else {
|
||||||
|
branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx));
|
||||||
|
buffer_entries.push(BufferEntry {
|
||||||
|
branch: branch_buffer.clone(),
|
||||||
|
base: location.buffer.clone(),
|
||||||
|
_subscription: cx.subscribe(&branch_buffer, Self::on_buffer_event),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.multibuffer.update(cx, |multibuffer, cx| {
|
||||||
|
multibuffer.push_excerpts(
|
||||||
|
branch_buffer,
|
||||||
|
location.ranges.into_iter().map(|range| ExcerptRange {
|
||||||
|
context: range,
|
||||||
|
primary: None,
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.buffer_entries = buffer_entries;
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
editor.change_selections(None, cx, |selections| selections.refresh())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recalculate_all_buffer_diffs(&self) {
|
||||||
|
for (ix, entry) in self.buffer_entries.iter().enumerate().rev() {
|
||||||
|
self.recalculate_diffs_tx
|
||||||
|
.unbounded_send(RecalculateDiff {
|
||||||
|
buffer: entry.branch.clone(),
|
||||||
|
debounce: ix > 0,
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,11 +248,11 @@ impl Item for ProposedChangesEditor {
|
||||||
type Event = EditorEvent;
|
type Event = EditorEvent;
|
||||||
|
|
||||||
fn tab_icon(&self, _cx: &ui::WindowContext) -> Option<Icon> {
|
fn tab_icon(&self, _cx: &ui::WindowContext) -> Option<Icon> {
|
||||||
Some(Icon::new(IconName::Pencil))
|
Some(Icon::new(IconName::Diff))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
|
fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
|
||||||
Some("Proposed changes".into())
|
Some(self.title.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||||
|
|
|
@ -434,12 +434,10 @@ impl<T> Clone for Model<T> {
|
||||||
|
|
||||||
impl<T> std::fmt::Debug for Model<T> {
|
impl<T> std::fmt::Debug for Model<T> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(
|
f.debug_struct("Model")
|
||||||
f,
|
.field("entity_id", &self.any_model.entity_id)
|
||||||
"Model {{ entity_id: {:?}, entity_type: {:?} }}",
|
.field("entity_type", &type_name::<T>())
|
||||||
self.any_model.entity_id,
|
.finish()
|
||||||
type_name::<T>()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -569,7 +567,10 @@ pub struct WeakModel<T> {
|
||||||
|
|
||||||
impl<T> std::fmt::Debug for WeakModel<T> {
|
impl<T> std::fmt::Debug for WeakModel<T> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.debug_struct(type_name::<WeakModel<T>>()).finish()
|
f.debug_struct(&type_name::<Self>())
|
||||||
|
.field("entity_id", &self.any_model.entity_id)
|
||||||
|
.field("entity_type", &type_name::<T>())
|
||||||
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ use anyhow::{anyhow, Context, Result};
|
||||||
use async_watch as watch;
|
use async_watch as watch;
|
||||||
use clock::Lamport;
|
use clock::Lamport;
|
||||||
pub use clock::ReplicaId;
|
pub use clock::ReplicaId;
|
||||||
|
use collections::HashMap;
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, AppContext, Context as _, EventEmitter, HighlightStyle, Model, ModelContext,
|
AnyElement, AppContext, Context as _, EventEmitter, HighlightStyle, Model, ModelContext,
|
||||||
|
@ -910,10 +911,8 @@ impl Buffer {
|
||||||
self.apply_ops([operation.clone()], cx);
|
self.apply_ops([operation.clone()], cx);
|
||||||
|
|
||||||
if let Some(timestamp) = operation_to_undo {
|
if let Some(timestamp) = operation_to_undo {
|
||||||
let operation = self
|
let counts = [(timestamp, u32::MAX)].into_iter().collect();
|
||||||
.text
|
self.undo_operations(counts, cx);
|
||||||
.undo_operations([(timestamp, u32::MAX)].into_iter().collect());
|
|
||||||
self.send_operation(Operation::Buffer(operation), true, cx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.diff_base_version += 1;
|
self.diff_base_version += 1;
|
||||||
|
@ -2331,6 +2330,18 @@ impl Buffer {
|
||||||
undone
|
undone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn undo_operations(
|
||||||
|
&mut self,
|
||||||
|
counts: HashMap<Lamport, u32>,
|
||||||
|
cx: &mut ModelContext<Buffer>,
|
||||||
|
) {
|
||||||
|
let was_dirty = self.is_dirty();
|
||||||
|
let operation = self.text.undo_operations(counts);
|
||||||
|
let old_version = self.version.clone();
|
||||||
|
self.send_operation(Operation::Buffer(operation), true, cx);
|
||||||
|
self.did_edit(&old_version, was_dirty, cx);
|
||||||
|
}
|
||||||
|
|
||||||
/// Manually redoes a specific transaction in the buffer's redo history.
|
/// Manually redoes a specific transaction in the buffer's redo history.
|
||||||
pub fn redo(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
|
pub fn redo(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
|
||||||
let was_dirty = self.is_dirty();
|
let was_dirty = self.is_dirty();
|
||||||
|
|
|
@ -9,7 +9,9 @@ license = "GPL-3.0-or-later"
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
test-support = []
|
test-support = [
|
||||||
|
"tree-sitter"
|
||||||
|
]
|
||||||
load-grammars = [
|
load-grammars = [
|
||||||
"tree-sitter-bash",
|
"tree-sitter-bash",
|
||||||
"tree-sitter-c",
|
"tree-sitter-c",
|
||||||
|
@ -75,6 +77,7 @@ tree-sitter-yaml = { workspace = true, optional = true }
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
tree-sitter.workspace = true
|
||||||
text.workspace = true
|
text.workspace = true
|
||||||
theme = { workspace = true, features = ["test-support"] }
|
theme = { workspace = true, features = ["test-support"] }
|
||||||
unindent.workspace = true
|
unindent.workspace = true
|
||||||
|
|
|
@ -1427,7 +1427,7 @@ impl Buffer {
|
||||||
fn undo_or_redo(&mut self, transaction: Transaction) -> Operation {
|
fn undo_or_redo(&mut self, transaction: Transaction) -> Operation {
|
||||||
let mut counts = HashMap::default();
|
let mut counts = HashMap::default();
|
||||||
for edit_id in transaction.edit_ids {
|
for edit_id in transaction.edit_ids {
|
||||||
counts.insert(edit_id, self.undo_map.undo_count(edit_id) + 1);
|
counts.insert(edit_id, self.undo_map.undo_count(edit_id).saturating_add(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
let operation = self.undo_operations(counts);
|
let operation = self.undo_operations(counts);
|
||||||
|
|
|
@ -170,6 +170,7 @@ pub enum IconName {
|
||||||
Dash,
|
Dash,
|
||||||
DatabaseZap,
|
DatabaseZap,
|
||||||
Delete,
|
Delete,
|
||||||
|
Diff,
|
||||||
Disconnected,
|
Disconnected,
|
||||||
Download,
|
Download,
|
||||||
Ellipsis,
|
Ellipsis,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue