Compare commits

..

2 Commits

Author SHA1 Message Date
Antonio Scandurra
414ea980f9 Subtract FREE_TIER_MONTHLY_SPENDING_LIMIT from reported monthly spend 2024-10-17 12:36:28 +02:00
Antonio Scandurra
7b5f236811 Introduce a new /billing/monthly_spend API 2024-10-17 11:55:41 +02:00
50 changed files with 3048 additions and 2316 deletions

View File

@@ -7,3 +7,9 @@ runs:
- name: cargo fmt
shell: bash -euxo pipefail {0}
run: cargo fmt --all -- --check
- name: Find modified migrations
shell: bash -euxo pipefail {0}
run: |
export SQUAWK_GITHUB_TOKEN=${{ github.token }}
. ./script/squawk

View File

@@ -26,10 +26,9 @@ env:
RUST_BACKTRACE: 1
jobs:
migration_checks:
name: Check Postgres and Protobuf migrations, mergability
if: github.repository_owner == 'zed-industries'
style:
timeout-minutes: 60
name: Check formatting and spelling
runs-on:
- self-hosted
- test
@@ -38,16 +37,25 @@ jobs:
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false
fetch-depth: 0 # fetch full history
fetch-depth: 0
- name: Remove untracked files
run: git clean -df
- name: Find modified migrations
shell: bash -euxo pipefail {0}
run: |
export SQUAWK_GITHUB_TOKEN=${{ github.token }}
. ./script/squawk
- name: Check spelling
run: script/check-spelling
- name: Run style checks
uses: ./.github/actions/check_style
- name: Check unused dependencies
uses: bnjbvr/cargo-machete@main
- name: Check licenses are present
run: script/check-licenses
- name: Check license generation
run: script/generate-licenses /tmp/zed_licenses_output
- name: Ensure fresh merge
shell: bash -euxo pipefail {0}
@@ -69,24 +77,6 @@ jobs:
input: "crates/proto/proto/"
against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/"
style:
timeout-minutes: 60
name: Check formatting and spelling
if: github.repository_owner == 'zed-industries'
runs-on:
- buildjet-8vcpu-ubuntu-2204
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
- name: Run style checks
uses: ./.github/actions/check_style
- name: Check for typos
uses: crate-ci/typos@v1.24.6
with:
config: ./typos.toml
macos_tests:
timeout-minutes: 60
name: (macOS) Run Clippy and tests
@@ -102,14 +92,6 @@ jobs:
- name: cargo clippy
run: ./script/clippy
- name: Check unused dependencies
uses: bnjbvr/cargo-machete@main
- name: Check licenses
run: |
script/check-licenses
script/generate-licenses /tmp/zed_licenses_output
- name: Run tests
uses: ./.github/actions/run_tests

View File

@@ -8,7 +8,6 @@ on:
jobs:
deploy-docs:
name: Deploy Docs
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:

View File

@@ -11,7 +11,6 @@ on:
jobs:
check_formatting:
name: "Check formatting"
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:
@@ -30,8 +29,5 @@ jobs:
false
}
- name: Check for Typos with Typos-CLI
uses: crate-ci/typos@v1.24.6
with:
config: ./typos.toml
files: ./docs/
- name: Check spelling
run: script/check-spelling docs/

5
Cargo.lock generated
View File

@@ -412,7 +412,6 @@ dependencies = [
"parking_lot",
"paths",
"picker",
"pretty_assertions",
"project",
"proto",
"rand 0.8.5",
@@ -8473,7 +8472,6 @@ dependencies = [
"terminal",
"text",
"unindent",
"url",
"util",
"which 6.0.3",
"windows 0.58.0",
@@ -8975,7 +8973,6 @@ dependencies = [
"futures 0.3.30",
"fuzzy",
"gpui",
"itertools 0.13.0",
"language",
"log",
"menu",
@@ -9150,8 +9147,6 @@ dependencies = [
"env_logger",
"fs",
"futures 0.3.30",
"git",
"git_hosting_providers",
"gpui",
"http_client",
"language",

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 275 B

View File

@@ -1,33 +1,85 @@
<task_description>
The user of a code editor wants to make a change to their codebase.
You must describe the change using the following XML structure:
# Code Change Workflow
- <patch> - A group of related code changes.
Child tags:
- <title> (required) - A high-level description of the changes. This should be as short
as possible, possibly using common abbreviations.
- <edit> (1 or more) - An edit to make at a particular range within a file.
Includes the following child tags:
- <path> (required) - The path to the file that will be changed.
- <description> (optional) - An arbitrarily-long comment that describes the purpose
of this edit.
- <old_text> (optional) - An excerpt from the file's current contents that uniquely
identifies a range within the file where the edit should occur. If this tag is not
specified, then the entire file will be used as the range.
- <new_text> (required) - The new text to insert into the file.
- <operation> (required) - The type of change that should occur at the given range
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.
- `insert_after`: Inserts new text after the range.
- `create`: Creates a new file with the given path and the new text.
- `delete`: Deletes the specified range from the file.
Your task is to guide the user through code changes using a series of steps. Each step should describe a high-level change, which can consist of multiple edits to distinct locations in the codebase.
## Output Example
Provide output as XML, with the following format:
<step>
Update the Person struct to store an age
```rust
struct Person {
// existing fields...
age: u8,
height: f32,
// existing fields...
}
impl Person {
fn age(&self) -> u8 {
self.age
}
}
```
<edit>
<path>src/person.rs</path>
<operation>insert_before</operation>
<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>
- Never provide multiple edits whose ranges intersect each other. Instead, merge them into one edit.
- Prefer multiple edits to smaller, disjoint ranges, rather than one edit to a larger range.
- There's no need to escape angle brackets within XML tags.
- 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 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.
</guidelines>
@@ -72,137 +124,189 @@ Update all shapes to store their origin as an (x, y) tuple and implement Display
<message role="assistant">
We'll need to update both the rectangle and circle modules.
<patch>
<title>Add origins and display impls to shapes</title>
<edit>
<path>src/shapes/rectangle.rs</path>
<description>Add the origin field to Rectangle struct</description>
<operation>insert_after</operation>
<old_text>
pub struct Rectangle {
</old_text>
<new_text>
origin: (f64, f64),
</new_text>
</edit>
<step>
Add origin fields to both shape types.
<edit>
<path>src/shapes/rectangle.rs</path>
<description>Update the Rectangle's new function to take an origin parameter</description>
<operation>update</operation>
<old_text>
fn new(width: f64, height: f64) -> Self {
Rectangle { width, height }
}
</old_text>
<new_text>
fn new(origin: (f64, f64), width: f64, height: f64) -> Self {
Rectangle { origin, width, height }
}
</new_text>
</edit>
<edit>
<path>src/shapes/circle.rs</path>
<description>Add the origin field to Circle struct</description>
<operation>insert_after</operation>
<old_text>
pub struct Circle {
radius: f64,
</old_text>
<new_text>
```rust
struct Rectangle {
// existing fields ...
origin: (f64, f64),
</new_text>
}
```
```rust
struct Circle {
// existing fields ...
origin: (f64, f64),
}
```
<edit>
<path>src/shapes/rectangle.rs</path>
<operation>insert_before</operation>
<search>
width: f64,
height: f64,
</search>
<description>Add the origin field to Rectangle</description>
</edit>
<edit>
<path>src/shapes/circle.rs</path>
<description>Update the Circle's new function to take an origin parameter</description>
<operation>insert_before</operation>
<search>
radius: f64,
</search>
<description>Add the origin field to Circle</description>
</edit>
<step>
Update both shape's constructors to take an origin.
<edit>
<path>src/shapes/rectangle.rs</path>
<operation>update</operation>
<old_text>
fn new(radius: f64) -> Self {
Circle { radius }
}
</old_text>
<new_text>
fn new(origin: (f64, f64), radius: f64) -> Self {
Circle { origin, radius }
}
</new_text>
<search>
fn new(width: f64, height: f64) -> Self {
Rectangle { width, height }
}
</search>
<description>Update the Rectangle new function to take an origin</description>
</edit>
<edit>
<path>src/shapes/circle.rs</path>
<operation>update</operation>
<search>
fn new(radius: f64) -> Self {
Circle { radius }
}
</search>
<description>Update the Circle new function to take an origin</description>
</edit>
</step>
<step>
Implement Display for both shapes
<edit>
<path>src/shapes/rectangle.rs</path>
<description>Add an import for the std::fmt module</description>
<operation>insert_before</operation>
<old_text>
<search>
struct Rectangle {
</old_text>
<new_text>
use std::fmt;
</new_text>
</search>
<description>Add an import for the `std::fmt` module</description>
</edit>
<edit>
<path>src/shapes/rectangle.rs</path>
<description>Add a Display implementation for Rectangle</description>
<operation>insert_after</operation>
<old_text>
<search>
Rectangle { width, height }
}
}
</old_text>
<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>
</search>
<description>Add a Display implementation for Rectangle</description>
</edit>
<edit>
<path>src/shapes/circle.rs</path>
<description>Add an import for the `std::fmt` module</description>
<operation>insert_before</operation>
<old_text>
<search>
struct Circle {
</old_text>
<new_text>
use std::fmt;
</new_text>
</search>
<description>Add an import for the `std::fmt` module</description>
</edit>
<edit>
<path>src/shapes/circle.rs</path>
<description>Add a Display implementation for Circle</description>
<operation>insert_after</operation>
<old_text>
<search>
Circle { radius }
}
}
</old_text>
<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>
</search>
<description>Add a Display implementation for Circle</description>
</edit>
</patch>
</step>
</message>
</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>

View File

@@ -0,0 +1,496 @@
<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>

View File

@@ -712,10 +712,10 @@
// May take 2 values:
// 1. Rely on default platform handling of option key, on macOS
// this means generating certain unicode characters
// "option_as_meta": false,
// "option_to_meta": false,
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
// "option_as_meta": true,
"option_as_meta": false,
// "option_to_meta": true,
"option_as_meta": true,
// Whether or not selecting text in the terminal will automatically
// copy to the system clipboard.
"copy_on_select": false,

View File

@@ -97,7 +97,6 @@ language = { workspace = true, features = ["test-support"] }
language_model = { workspace = true, features = ["test-support"] }
languages = { workspace = true, features = ["test-support"] }
log.workspace = true
pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
serde_json_lenient.workspace = true

View File

@@ -6,7 +6,6 @@ mod context;
pub mod context_store;
mod inline_assistant;
mod model_selector;
mod patch;
mod prompt_library;
mod prompts;
mod slash_command;
@@ -15,6 +14,7 @@ pub mod slash_command_settings;
mod streaming_diff;
mod terminal_inline_assistant;
mod tools;
mod workflow;
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
use assistant_settings::AssistantSettings;
@@ -35,13 +35,11 @@ use language_model::{
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
};
pub(crate) use model_selector::*;
pub use patch::*;
pub use prompts::PromptBuilder;
use prompts::PromptLoadingParams;
use semantic_index::{CloudEmbeddingProvider, SemanticDb};
use serde::{Deserialize, Serialize};
use settings::{update_settings_file, Settings, SettingsStore};
use slash_command::workflow_command::WorkflowSlashCommand;
use slash_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,
@@ -52,15 +50,14 @@ use std::path::PathBuf;
use std::sync::Arc;
pub(crate) use streaming_diff::*;
use util::ResultExt;
pub use workflow::*;
use crate::slash_command_settings::SlashCommandSettings;
actions!(
assistant,
[
AssistLegacy,
AssistChat,
AssistEdit,
Assist,
Split,
CopyCode,
CycleMessageRole,
@@ -396,25 +393,12 @@ 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(diagnostics_command::DiagnosticsSlashCommand, true);
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 {
cx.observe_global::<SettingsStore>({
let slash_command_registry = slash_command_registry.clone();
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();
slash_command_registry.register_command(
workflow_command::WorkflowSlashCommand::new(prompt_builder.clone()),
true,
);
cx.observe_flag::<project_command::ProjectSlashCommandFeatureFlag, _>({
let slash_command_registry = slash_command_registry.clone();
move |is_enabled, _cx| {

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@ use std::sync::Arc;
use ::open_ai::Model as OpenAiModel;
use anthropic::Model as AnthropicModel;
use feature_flags::FeatureFlagAppExt;
use fs::Fs;
use gpui::{AppContext, Pixels};
use language_model::provider::open_ai;
@@ -62,13 +61,6 @@ pub struct AssistantSettings {
pub default_model: LanguageModelSelection,
pub inline_alternatives: Vec<LanguageModelSelection>,
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
@@ -246,7 +238,6 @@ impl AssistantSettingsContent {
}
}),
inline_alternatives: None,
enable_experimental_live_diffs: None,
},
VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
},
@@ -266,7 +257,6 @@ impl AssistantSettingsContent {
.to_string(),
}),
inline_alternatives: None,
enable_experimental_live_diffs: None,
},
}
}
@@ -383,7 +373,6 @@ impl Default for VersionedAssistantSettingsContent {
default_height: None,
default_model: None,
inline_alternatives: None,
enable_experimental_live_diffs: None,
})
}
}
@@ -414,10 +403,6 @@ pub struct AssistantSettingsContentV2 {
default_model: Option<LanguageModelSelection>,
/// Additional models with which to generate alternatives when performing inline assists.
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)]
@@ -540,10 +525,7 @@ impl Settings for AssistantSettings {
);
merge(&mut settings.default_model, value.default_model);
merge(&mut settings.inline_alternatives, value.inline_alternatives);
merge(
&mut settings.enable_experimental_live_diffs,
value.enable_experimental_live_diffs,
);
// merge(&mut settings.infer_context, value.infer_context); TODO re-enable this once we ship context inference
}
Ok(settings)
@@ -602,7 +584,6 @@ mod tests {
dock: None,
default_width: None,
default_height: None,
enable_experimental_live_diffs: None,
}),
)
},

View File

@@ -2,8 +2,8 @@
mod context_tests;
use crate::{
prompts::PromptBuilder, slash_command::SlashCommandLine, AssistantEdit, AssistantPatch,
AssistantPatchStatus, MessageId, MessageStatus,
prompts::PromptBuilder, slash_command::SlashCommandLine, MessageId, MessageStatus,
WorkflowStep, WorkflowStepEdit, WorkflowStepResolution, WorkflowSuggestionGroup,
};
use anyhow::{anyhow, Context as _, Result};
use assistant_slash_command::{
@@ -15,10 +15,13 @@ use clock::ReplicaId;
use collections::{HashMap, HashSet};
use feature_flags::{FeatureFlag, FeatureFlagAppExt};
use fs::{Fs, RemoveOptions};
use futures::{future::Shared, FutureExt, StreamExt};
use futures::{
future::{self, Shared},
FutureExt, StreamExt,
};
use gpui::{
AppContext, Context as _, EventEmitter, Model, ModelContext, RenderImage, SharedString,
Subscription, Task,
AppContext, AsyncAppContext, Context as _, EventEmitter, Model, ModelContext, RenderImage,
SharedString, Subscription, Task,
};
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
@@ -35,7 +38,7 @@ use project::Project;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use std::{
cmp::{max, Ordering},
cmp::{self, max, Ordering},
fmt::Debug,
iter, mem,
ops::Range,
@@ -113,10 +116,6 @@ impl ContextOperation {
message.status.context("invalid status")?,
),
timestamp: id.0,
// kind: {
// let todo = (); // TODO: Should these go in the protocol?
// MessageKind::Legacy
// },
cache: None,
},
version: language::proto::deserialize_version(&insert.version),
@@ -132,10 +131,6 @@ impl ContextOperation {
timestamp: language::proto::deserialize_timestamp(
update.timestamp.context("invalid timestamp")?,
),
// kind: {
// let todo = (); // TODO: Should these go in the protocol?
// MessageKind::Legacy
// },
cache: None,
},
version: language::proto::deserialize_version(&update.version),
@@ -305,7 +300,7 @@ pub enum ContextEvent {
MessagesEdited,
SummaryChanged,
StreamedCompletion,
PatchesUpdated {
WorkflowStepsUpdated {
removed: Vec<Range<language::Anchor>>,
updated: Vec<Range<language::Anchor>>,
},
@@ -359,28 +354,16 @@ pub struct MessageMetadata {
pub role: Role,
pub status: MessageStatus,
pub(crate) timestamp: clock::Lamport,
// pub kind: MessageKind,
#[serde(skip)]
pub cache: Option<MessageCacheMetadata>,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum MessageKind {
/// No preamble
Legacy,
/// Preamble along the lines of "This is a chat message:"
Chat,
/// Preamble along the lines of "This is an edit message:"
Edit,
}
impl From<&Message> for MessageMetadata {
fn from(message: &Message) -> Self {
Self {
role: message.role,
status: message.status.clone(),
timestamp: message.id.0,
// kind: message.kind,
cache: message.cache.clone(),
}
}
@@ -410,7 +393,6 @@ pub struct Message {
pub id: MessageId,
pub role: Role,
pub status: MessageStatus,
pub kind: MessageKind,
pub cache: Option<MessageCacheMetadata>,
}
@@ -472,14 +454,13 @@ pub struct XmlTag {
#[derive(Copy, Clone, Debug, strum::EnumString, PartialEq, Eq, strum::AsRefStr)]
#[strum(serialize_all = "snake_case")]
pub enum XmlTagKind {
Patch,
Title,
Step,
Edit,
Path,
Description,
OldText,
NewText,
Search,
Within,
Operation,
Description,
}
pub struct Context {
@@ -509,7 +490,7 @@ pub struct Context {
_subscriptions: Vec<Subscription>,
telemetry: Option<Arc<Telemetry>>,
language_registry: Arc<LanguageRegistry>,
patches: Vec<AssistantPatch>,
workflow_steps: Vec<WorkflowStep>,
xml_tags: Vec<XmlTag>,
project: Option<Model<Project>>,
prompt_builder: Arc<PromptBuilder>,
@@ -525,7 +506,7 @@ impl ContextAnnotation for PendingSlashCommand {
}
}
impl ContextAnnotation for AssistantPatch {
impl ContextAnnotation for WorkflowStep {
fn range(&self) -> &Range<language::Anchor> {
&self.range
}
@@ -610,7 +591,7 @@ impl Context {
telemetry,
project,
language_registry,
patches: Vec::new(),
workflow_steps: Vec::new(),
xml_tags: Vec::new(),
prompt_builder,
};
@@ -948,49 +929,48 @@ impl Context {
self.summary.as_ref()
}
pub(crate) fn patch_containing(
pub(crate) fn workflow_step_containing(
&self,
position: Point,
offset: usize,
cx: &AppContext,
) -> Option<&AssistantPatch> {
) -> Option<&WorkflowStep> {
let buffer = self.buffer.read(cx);
let index = self.patches.binary_search_by(|patch| {
let patch_range = patch.range.to_point(&buffer);
if position < patch_range.start {
Ordering::Greater
} else if position > patch_range.end {
Ordering::Less
} else {
Ordering::Equal
}
});
if let Ok(ix) = index {
Some(&self.patches[ix])
} else {
None
}
let index = self
.workflow_steps
.binary_search_by(|step| {
let step_range = step.range.to_offset(&buffer);
if offset < step_range.start {
Ordering::Greater
} else if offset > step_range.end {
Ordering::Less
} else {
Ordering::Equal
}
})
.ok()?;
Some(&self.workflow_steps[index])
}
pub fn patch_ranges(&self) -> impl Iterator<Item = Range<language::Anchor>> + '_ {
self.patches.iter().map(|patch| patch.range.clone())
pub fn workflow_step_ranges(&self) -> impl Iterator<Item = Range<language::Anchor>> + '_ {
self.workflow_steps.iter().map(|step| step.range.clone())
}
pub(crate) fn patch_for_range(
pub(crate) fn workflow_step_for_range(
&self,
range: &Range<language::Anchor>,
cx: &AppContext,
) -> Option<&AssistantPatch> {
) -> Option<&WorkflowStep> {
let buffer = self.buffer.read(cx);
let index = self.patch_index_for_range(range, buffer).ok()?;
Some(&self.patches[index])
let index = self.workflow_step_index_for_range(range, buffer).ok()?;
Some(&self.workflow_steps[index])
}
fn patch_index_for_range(
fn workflow_step_index_for_range(
&self,
tagged_range: &Range<text::Anchor>,
buffer: &text::BufferSnapshot,
) -> Result<usize, usize> {
self.patches
self.workflow_steps
.binary_search_by(|probe| probe.range.cmp(&tagged_range, buffer))
}
@@ -1038,6 +1018,8 @@ impl Context {
language::BufferEvent::Edited => {
self.count_remaining_tokens(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);
}
_ => {}
@@ -1266,8 +1248,8 @@ impl Context {
let mut removed_slash_command_ranges = Vec::new();
let mut updated_slash_commands = Vec::new();
let mut removed_patches = Vec::new();
let mut updated_patches = Vec::new();
let mut removed_steps = Vec::new();
let mut updated_steps = Vec::new();
while let Some(mut row_range) = row_ranges.next() {
while let Some(next_row_range) = row_ranges.peek() {
if row_range.end >= next_row_range.start {
@@ -1291,11 +1273,11 @@ impl Context {
&mut removed_slash_command_ranges,
cx,
);
self.reparse_patches_in_range(
self.reparse_workflow_steps_in_range(
start..end,
&buffer,
&mut updated_patches,
&mut removed_patches,
&mut updated_steps,
&mut removed_steps,
cx,
);
}
@@ -1307,10 +1289,10 @@ impl Context {
});
}
if !updated_patches.is_empty() || !removed_patches.is_empty() {
cx.emit(ContextEvent::PatchesUpdated {
removed: removed_patches,
updated: updated_patches,
if !updated_steps.is_empty() || !removed_steps.is_empty() {
cx.emit(ContextEvent::WorkflowStepsUpdated {
removed: removed_steps,
updated: updated_steps,
});
}
}
@@ -1372,7 +1354,7 @@ impl Context {
removed.extend(removed_commands.map(|command| command.source_range));
}
fn reparse_patches_in_range(
fn reparse_workflow_steps_in_range(
&mut self,
range: Range<text::Anchor>,
buffer: &BufferSnapshot,
@@ -1387,32 +1369,41 @@ impl Context {
self.xml_tags
.splice(intersecting_tags_range.clone(), new_tags);
// Find which patches intersect the changed range.
let intersecting_patches_range =
self.indices_intersecting_buffer_range(&self.patches, range.clone(), cx);
// Find which steps intersect the changed range.
let intersecting_steps_range =
self.indices_intersecting_buffer_range(&self.workflow_steps, range.clone(), cx);
// Reparse all tags after the last unchanged patch before the change.
// Reparse all tags after the last unchanged step before the change.
let mut tags_start_ix = 0;
if let Some(preceding_unchanged_patch) =
self.patches[..intersecting_patches_range.start].last()
if let Some(preceding_unchanged_step) =
self.workflow_steps[..intersecting_steps_range.start].last()
{
tags_start_ix = match self.xml_tags.binary_search_by(|tag| {
tag.range
.start
.cmp(&preceding_unchanged_patch.range.end, buffer)
.cmp(&preceding_unchanged_step.range.end, buffer)
.then(Ordering::Less)
}) {
Ok(ix) | Err(ix) => ix,
};
}
// Rebuild the patches in the range.
let new_patches = self.parse_patches(tags_start_ix, range.end, buffer, cx);
updated.extend(new_patches.iter().map(|patch| patch.range.clone()));
let removed_patches = self.patches.splice(intersecting_patches_range, new_patches);
// Rebuild the edit suggestions in the range.
let mut new_steps = self.parse_steps(tags_start_ix, range.end, buffer);
if let Some(project) = self.project() {
for step in &mut new_steps {
Self::resolve_workflow_step_internal(step, &project, cx);
}
}
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_patches
.map(|patch| patch.range)
removed_steps
.map(|step| step.range)
.filter(|range| !updated.contains(&range)),
);
}
@@ -1473,95 +1464,60 @@ impl Context {
tags
}
fn parse_patches(
fn parse_steps(
&mut self,
tags_start_ix: usize,
buffer_end: text::Anchor,
buffer: &BufferSnapshot,
cx: &AppContext,
) -> Vec<AssistantPatch> {
let mut new_patches = Vec::new();
let mut pending_patch = None;
let mut patch_tag_depth = 0;
) -> Vec<WorkflowStep> {
let mut new_steps = Vec::new();
let mut pending_step = None;
let mut edit_step_depth = 0;
let mut tags = self.xml_tags[tags_start_ix..].iter().peekable();
'tags: while let Some(tag) = tags.next() {
if tag.range.start.cmp(&buffer_end, buffer).is_gt() && patch_tag_depth == 0 {
if tag.range.start.cmp(&buffer_end, buffer).is_gt() && edit_step_depth == 0 {
break;
}
if tag.kind == XmlTagKind::Patch && tag.is_open_tag {
patch_tag_depth += 1;
let patch_start = tag.range.start;
let mut edits = Vec::<Result<AssistantEdit>>::new();
let mut patch = AssistantPatch {
range: patch_start..patch_start,
title: String::new().into(),
if tag.kind == XmlTagKind::Step && tag.is_open_tag {
edit_step_depth += 1;
let edit_start = tag.range.start;
let mut edits = Vec::new();
let mut step = WorkflowStep {
range: edit_start..edit_start,
leading_tags_end: tag.range.end,
trailing_tag_start: None,
edits: Default::default(),
status: crate::AssistantPatchStatus::Pending,
resolution: None,
resolution_task: None,
};
while let Some(tag) = tags.next() {
if tag.kind == XmlTagKind::Patch && !tag.is_open_tag {
patch_tag_depth -= 1;
if patch_tag_depth == 0 {
patch.range.end = tag.range.end;
step.trailing_tag_start.get_or_insert(tag.range.start);
// Include the line immediately after this <patch> tag if it's empty.
let patch_end_offset = patch.range.end.to_offset(buffer);
let mut patch_end_chars = buffer.chars_at(patch_end_offset);
if patch_end_chars.next() == Some('\n')
&& patch_end_chars.next().map_or(true, |ch| ch == '\n')
{
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);
if tag.kind == XmlTagKind::Step && !tag.is_open_tag {
// step.trailing_tag_start = Some(tag.range.start);
edit_step_depth -= 1;
if edit_step_depth == 0 {
step.range.end = tag.range.end;
step.edits = edits.into();
new_steps.push(step);
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 {
let mut path = None;
let mut old_text = None;
let mut new_text = None;
let mut search = None;
let mut operation = None;
let mut description = None;
while let Some(tag) = tags.next() {
if tag.kind == XmlTagKind::Edit && !tag.is_open_tag {
edits.push(AssistantEdit::new(
edits.push(WorkflowStepEdit::new(
path,
operation,
old_text,
new_text,
search,
description,
));
break;
@@ -1570,8 +1526,7 @@ impl Context {
if tag.is_open_tag
&& [
XmlTagKind::Path,
XmlTagKind::OldText,
XmlTagKind::NewText,
XmlTagKind::Search,
XmlTagKind::Operation,
XmlTagKind::Description,
]
@@ -1583,18 +1538,15 @@ impl Context {
if tag.kind == kind && !tag.is_open_tag {
let tag = tags.next().unwrap();
let content_end = tag.range.start;
let content = trimmed_text_in_range(
buffer,
content_start..content_end,
);
let mut content = buffer
.text_for_range(content_start..content_end)
.collect::<String>();
content.truncate(content.trim_end().len());
match kind {
XmlTagKind::Path => path = Some(content),
XmlTagKind::Operation => operation = Some(content),
XmlTagKind::OldText => {
old_text = Some(content).filter(|s| !s.is_empty())
}
XmlTagKind::NewText => {
new_text = Some(content).filter(|s| !s.is_empty())
XmlTagKind::Search => {
search = Some(content).filter(|s| !s.is_empty())
}
XmlTagKind::Description => {
description =
@@ -1609,28 +1561,162 @@ impl Context {
}
}
patch.edits = edits.into();
pending_patch = Some(patch);
pending_step = Some(step);
}
}
if let Some(mut pending_patch) = pending_patch {
let patch_start = pending_patch.range.start.to_offset(buffer);
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;
if let Some(mut pending_step) = pending_step {
pending_step.range.end = text::Anchor::MAX;
new_steps.push(pending_step);
}
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 {
let message_end = buffer.anchor_after(message.offset_range.end - 1);
pending_patch.range.end = message_end;
// Create the first group
suggestion_groups.push(WorkflowSuggestionGroup {
context_range,
suggestions: vec![suggestion],
});
}
} else {
pending_patch.range.end = text::Anchor::MAX;
}
new_patches.push(pending_patch);
suggestion_groups_by_buffer.insert(buffer, suggestion_groups);
}
new_patches
Ok(suggestion_groups_by_buffer)
}
pub fn pending_command_for_position(
@@ -1886,11 +1972,7 @@ impl Context {
})
}
pub fn assist(
&mut self,
message_kind: MessageKind,
cx: &mut ModelContext<Self>,
) -> Option<MessageAnchor> {
pub fn assist(&mut self, cx: &mut ModelContext<Self>) -> Option<MessageAnchor> {
let model_registry = LanguageModelRegistry::read_global(cx);
let provider = model_registry.active_provider()?;
let model = model_registry.active_model()?;
@@ -1900,14 +1982,6 @@ impl Context {
log::info!("completion provider has no credentials");
return None;
}
let last_message = self
.messages(cx)
.find(|message| message.id == last_message_id);
// Mutate this so that future completion requests include past preambles too.
last_message.kind = message_kind;
// Compute which messages to cache, including the last one.
self.mark_cache_anchors(&model.cache_configuration(), false, cx);
@@ -2241,11 +2315,11 @@ impl Context {
let mut updated = Vec::new();
let mut removed = Vec::new();
for range in ranges {
self.reparse_patches_in_range(range, &buffer, &mut updated, &mut removed, cx);
self.reparse_workflow_steps_in_range(range, &buffer, &mut updated, &mut removed, cx);
}
if !updated.is_empty() || !removed.is_empty() {
cx.emit(ContextEvent::PatchesUpdated { removed, updated })
cx.emit(ContextEvent::WorkflowStepsUpdated { removed, updated })
}
}
@@ -2751,24 +2825,6 @@ 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)]
pub struct ContextVersion {
context: clock::Global,

View File

@@ -1,7 +1,8 @@
use super::{AssistantEdit, MessageCacheMetadata};
use super::{MessageCacheMetadata, WorkflowStepEdit};
use crate::{
assistant_panel, prompt_library, slash_command::file_command, AssistantEditKind, CacheStatus,
Context, ContextEvent, ContextId, ContextOperation, MessageId, MessageStatus, PromptBuilder,
assistant_panel, prompt_library, slash_command::file_command, CacheStatus, Context,
ContextEvent, ContextId, ContextOperation, MessageId, MessageStatus, PromptBuilder,
WorkflowStepEditKind,
};
use anyhow::Result;
use assistant_slash_command::{
@@ -14,7 +15,6 @@ use gpui::{AppContext, Model, SharedString, Task, TestAppContext, WeakView};
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
use parking_lot::Mutex;
use pretty_assertions::assert_eq;
use project::Project;
use rand::prelude::*;
use serde_json::json;
@@ -478,15 +478,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
cx.update(prompt_library::init);
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()
});
let settings_store = cx.update(SettingsStore::test);
cx.set_global(settings_store);
cx.update(language::init);
cx.update(Project::init_settings);
@@ -528,7 +520,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
»",
cx,
);
expect_patches(
expect_steps(
&context,
"
@@ -547,17 +539,17 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
one
two
«
<patch»",
<step»",
cx,
);
expect_patches(
expect_steps(
&context,
"
one
two
<patch",
<step",
&[],
cx,
);
@@ -571,24 +563,36 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
one
two
<patch«>
<step«>
Add a second function
```rust
fn two() {}
```
<edit>»",
cx,
);
expect_patches(
expect_steps(
&context,
"
one
two
«<patch>
«<step>
Add a second function
```rust
fn two() {}
```
<edit>»",
&[&[]],
cx,
);
// The full patch is added
// The full suggestion is added
edit(
&context,
"
@@ -596,46 +600,51 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
one
two
<patch>
<step>
Add a second function
```rust
fn two() {}
```
<edit>«
<description>add a `two` function</description>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<old_text>fn one</old_text>
<new_text>
fn two() {}
</new_text>
<search>fn one</search>
<description>add a `two` function</description>
</edit>
</patch>
</step>
also,»",
cx,
);
expect_patches(
expect_steps(
&context,
"
one
two
«<patch>
«<step>
Add a second function
```rust
fn two() {}
```
<edit>
<description>add a `two` function</description>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<old_text>fn one</old_text>
<new_text>
fn two() {}
</new_text>
<search>fn one</search>
<description>add a `two` function</description>
</edit>
</patch>
»
</step>»
also,",
&[&[AssistantEdit {
&[&[WorkflowStepEdit {
path: "src/lib.rs".into(),
kind: AssistantEditKind::InsertAfter {
old_text: "fn one".into(),
new_text: "fn two() {}".into(),
kind: WorkflowStepEditKind::InsertAfter {
search: "fn one".into(),
description: "add a `two` function".into(),
},
}]],
@@ -650,46 +659,51 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
one
two
<patch>
<step>
Add a second function
```rust
fn two() {}
```
<edit>
<description>add a `two` function</description>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<old_text>«fn zero»</old_text>
<new_text>
fn two() {}
</new_text>
<search>«fn zero»</search>
<description>add a `two` function</description>
</edit>
</patch>
</step>
also,",
cx,
);
expect_patches(
expect_steps(
&context,
"
one
two
«<patch>
«<step>
Add a second function
```rust
fn two() {}
```
<edit>
<description>add a `two` function</description>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<old_text>fn zero</old_text>
<new_text>
fn two() {}
</new_text>
<search>fn zero</search>
<description>add a `two` function</description>
</edit>
</patch>
»
</step>»
also,",
&[&[AssistantEdit {
&[&[WorkflowStepEdit {
path: "src/lib.rs".into(),
kind: AssistantEditKind::InsertAfter {
old_text: "fn zero".into(),
new_text: "fn two() {}".into(),
kind: WorkflowStepEditKind::InsertAfter {
search: "fn zero".into(),
description: "add a `two` function".into(),
},
}]],
@@ -701,24 +715,27 @@ 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);
});
expect_patches(
expect_steps(
&context,
"
one
two
<patch>
<step>
Add a second function
```rust
fn two() {}
```
<edit>
<description>add a `two` function</description>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<old_text>fn zero</old_text>
<new_text>
fn two() {}
</new_text>
<search>fn zero</search>
<description>add a `two` function</description>
</edit>
</patch>
</step>
also,",
&[],
@@ -729,31 +746,33 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
context.update(cx, |context, cx| {
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
});
expect_patches(
expect_steps(
&context,
"
one
two
«<patch>
«<step>
Add a second function
```rust
fn two() {}
```
<edit>
<description>add a `two` function</description>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<old_text>fn zero</old_text>
<new_text>
fn two() {}
</new_text>
<search>fn zero</search>
<description>add a `two` function</description>
</edit>
</patch>
»
</step>»
also,",
&[&[AssistantEdit {
&[&[WorkflowStepEdit {
path: "src/lib.rs".into(),
kind: AssistantEditKind::InsertAfter {
old_text: "fn zero".into(),
new_text: "fn two() {}".into(),
kind: WorkflowStepEditKind::InsertAfter {
search: "fn zero".into(),
description: "add a `two` function".into(),
},
}]],
@@ -773,31 +792,33 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
cx,
)
});
expect_patches(
expect_steps(
&deserialized_context,
"
one
two
«<patch>
«<step>
Add a second function
```rust
fn two() {}
```
<edit>
<description>add a `two` function</description>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<old_text>fn zero</old_text>
<new_text>
fn two() {}
</new_text>
<search>fn zero</search>
<description>add a `two` function</description>
</edit>
</patch>
»
</step>»
also,",
&[&[AssistantEdit {
&[&[WorkflowStepEdit {
path: "src/lib.rs".into(),
kind: AssistantEditKind::InsertAfter {
old_text: "fn zero".into(),
new_text: "fn two() {}".into(),
kind: WorkflowStepEditKind::InsertAfter {
search: "fn zero".into(),
description: "add a `two` function".into(),
},
}]],
@@ -813,58 +834,48 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
cx.executor().run_until_parked();
}
#[track_caller]
fn expect_patches(
fn expect_steps(
context: &Model<Context>,
expected_marked_text: &str,
expected_suggestions: &[&[AssistantEdit]],
expected_suggestions: &[&[WorkflowStepEdit]],
cx: &mut TestAppContext,
) {
let expected_marked_text = expected_marked_text.unindent();
let (expected_text, _) = marked_text_ranges(&expected_marked_text, false);
let (buffer_text, ranges, patches) = context.update(cx, |context, cx| {
context.update(cx, |context, cx| {
let expected_marked_text = expected_marked_text.unindent();
let (expected_text, expected_ranges) = marked_text_ranges(&expected_marked_text, false);
context.buffer.read_with(cx, |buffer, _| {
assert_eq!(buffer.text(), expected_text);
let ranges = context
.patches
.workflow_steps
.iter()
.map(|entry| entry.range.to_offset(buffer))
.collect::<Vec<_>>();
(
buffer.text(),
ranges,
context
.patches
.iter()
.map(|step| step.edits.clone())
.collect::<Vec<_>>(),
)
})
let marked = generate_marked_text(&expected_text, &ranges, false);
assert_eq!(
marked,
expected_marked_text,
"unexpected suggestion ranges. actual: {ranges:?}, expected: {expected_ranges:?}"
);
let suggestions = context
.workflow_steps
.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
);
}
}

View File

@@ -82,6 +82,13 @@ pub struct InlineAssistant {
assists: HashMap<InlineAssistId, InlineAssist>,
assists_by_editor: HashMap<WeakView<Editor>, EditorInlineAssists>,
assist_groups: HashMap<InlineAssistGroupId, InlineAssistGroup>,
assist_observations: HashMap<
InlineAssistId,
(
async_watch::Sender<AssistStatus>,
async_watch::Receiver<AssistStatus>,
),
>,
confirmed_assists: HashMap<InlineAssistId, Model<CodegenAlternative>>,
prompt_history: VecDeque<String>,
prompt_builder: Arc<PromptBuilder>,
@@ -89,6 +96,19 @@ pub struct InlineAssistant {
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 InlineAssistant {
@@ -103,6 +123,7 @@ impl InlineAssistant {
assists: HashMap::default(),
assists_by_editor: HashMap::default(),
assist_groups: HashMap::default(),
assist_observations: HashMap::default(),
confirmed_assists: HashMap::default(),
prompt_history: VecDeque::default(),
prompt_builder,
@@ -814,6 +835,17 @@ impl InlineAssistant {
.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 {
@@ -1007,6 +1039,10 @@ impl InlineAssistant {
codegen.start(user_prompt, assistant_panel_context, cx)
})
.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) {
@@ -1017,6 +1053,25 @@ impl InlineAssistant {
};
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) {
@@ -1202,6 +1257,42 @@ impl InlineAssistant {
.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 {
@@ -2199,6 +2290,8 @@ impl InlineAssist {
if assist.decorations.is_none() {
this.finish_assist(assist_id, false, cx);
} else if let Some(tx) = this.assist_observations.get(&assist_id) {
tx.0.send(AssistStatus::Finished).ok();
}
}
})

View File

@@ -1,746 +0,0 @@
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()
}
}

View File

@@ -45,6 +45,15 @@ pub struct ProjectSlashCommandPromptContext {
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 fs: Arc<dyn Fs>,
pub repo_path: Option<PathBuf>,

View File

@@ -18,8 +18,6 @@ pub(crate) struct WorkflowSlashCommand {
}
impl WorkflowSlashCommand {
pub const NAME: &'static str = "workflow";
pub fn new(prompt_builder: Arc<PromptBuilder>) -> Self {
Self { prompt_builder }
}
@@ -27,7 +25,7 @@ impl WorkflowSlashCommand {
impl SlashCommand for WorkflowSlashCommand {
fn name(&self) -> String {
Self::NAME.into()
"workflow".into()
}
fn description(&self) -> String {

View File

@@ -0,0 +1,507 @@
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)
);
}
}
}

View File

@@ -10,8 +10,6 @@
Copy,
derive_more::Add,
derive_more::AddAssign,
derive_more::Sub,
derive_more::SubAssign,
)]
pub struct Cents(pub u32);

View File

@@ -469,9 +469,7 @@ async fn check_usage_limit(
));
}
if (usage.spending_this_month - FREE_TIER_MONTHLY_SPENDING_LIMIT)
>= Cents(claims.max_monthly_spend_in_cents)
{
if usage.spending_this_month >= Cents(claims.max_monthly_spend_in_cents) {
return Err(Error::Http(
StatusCode::FORBIDDEN,
"Maximum spending limit reached for this month.".to_string(),

View File

@@ -412,7 +412,7 @@ impl LlmDatabase {
if !is_staff
&& spending_this_month > FREE_TIER_MONTHLY_SPENDING_LIMIT
&& has_llm_subscription
&& (spending_this_month - FREE_TIER_MONTHLY_SPENDING_LIMIT) <= max_monthly_spend
&& spending_this_month <= max_monthly_spend
{
billing_event::ActiveModel {
id: ActiveValue::not_set(),

View File

@@ -1,7 +1,10 @@
use crate::{
db::UserId,
llm::{
db::{queries::providers::ModelParams, LlmDatabase, TokenUsage},
db::{
queries::{providers::ModelParams, usages::Usage},
LlmDatabase, TokenUsage,
},
FREE_TIER_MONTHLY_SPENDING_LIMIT,
},
test_llm_db, Cents,
@@ -73,9 +76,29 @@ async fn test_billing_limit_exceeded(db: &mut LlmDatabase) {
// Verify the recorded usage and spending
let recorded_usage = db.get_usage(user_id, provider, model, now).await.unwrap();
// Verify that we exceeded the free tier usage
assert_eq!(recorded_usage.spending_this_month, Cents::new(1050));
assert!(recorded_usage.spending_this_month > FREE_TIER_MONTHLY_SPENDING_LIMIT);
assert!(
recorded_usage.spending_this_month > FREE_TIER_MONTHLY_SPENDING_LIMIT,
"Expected spending to exceed free tier limit"
);
assert_eq!(
recorded_usage,
Usage {
requests_this_minute: 1,
tokens_this_minute: tokens_to_use,
tokens_this_day: tokens_to_use,
tokens_this_month: TokenUsage {
input: tokens_to_use,
input_cache_creation: 0,
input_cache_read: 0,
output: 0,
},
spending_this_month: Cents::new(1050),
lifetime_spending: Cents::new(1050),
}
);
// Verify that there is one `billing_event` record
let billing_events = db.get_billing_events().await.unwrap();
@@ -88,35 +111,7 @@ async fn test_billing_limit_exceeded(db: &mut LlmDatabase) {
assert_eq!(billing_event.input_cache_read_tokens, 0);
assert_eq!(billing_event.output_tokens, 0);
// Record usage that puts us at $20.50
let usage_2 = TokenUsage {
input: 200_000_000, // This will cost $10 more, pushing us from $10.50 to $20.50,
input_cache_creation: 0,
input_cache_read: 0,
output: 0,
};
db.record_usage(
user_id,
false,
provider,
model,
usage_2,
true,
max_monthly_spend,
now,
)
.await
.unwrap();
// Verify the updated usage and spending
let updated_usage = db.get_usage(user_id, provider, model, now).await.unwrap();
assert_eq!(updated_usage.spending_this_month, Cents::new(2050));
// Verify that there are now two billing events
let billing_events = db.get_billing_events().await.unwrap();
assert_eq!(billing_events.len(), 2);
let tokens_to_exceed = 20_000_000; // This will cost $1.00 more, pushing us from $20.50 to $21.50, which is over the $11 monthly maximum limit
let tokens_to_exceed = 20_000_000; // This will cost $1.00 more, pushing us from $10.50 to $11.50, which is over the $11 monthly maximum limit
let usage_exceeding = TokenUsage {
input: tokens_to_exceed,
input_cache_creation: 0,
@@ -137,12 +132,27 @@ async fn test_billing_limit_exceeded(db: &mut LlmDatabase) {
)
.await
.unwrap();
// Verify that there is still one billing record
let billing_events = db.get_billing_events().await.unwrap();
assert_eq!(billing_events.len(), 1);
// Verify the updated usage and spending
let updated_usage = db.get_usage(user_id, provider, model, now).await.unwrap();
assert_eq!(updated_usage.spending_this_month, Cents::new(2150));
// Verify that we never exceed the user max spending for the user
// and avoid charging them.
let billing_events = db.get_billing_events().await.unwrap();
assert_eq!(billing_events.len(), 2);
assert_eq!(
updated_usage,
Usage {
requests_this_minute: 2,
tokens_this_minute: tokens_to_use + tokens_to_exceed,
tokens_this_day: tokens_to_use + tokens_to_exceed,
tokens_this_month: TokenUsage {
input: tokens_to_use + tokens_to_exceed,
input_cache_creation: 0,
input_cache_read: 0,
output: 0,
},
spending_this_month: Cents::new(1150),
lifetime_spending: Cents::new(1150),
}
);
}

View File

@@ -2,7 +2,7 @@ use std::sync::Arc;
use crate::{llm, Cents, Result};
use anyhow::Context;
use chrono::{Datelike, Utc};
use chrono::Utc;
use collections::HashMap;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
@@ -349,20 +349,10 @@ impl StripeBilling {
model: &StripeModel,
success_url: &str,
) -> Result<String> {
let first_of_next_month = Utc::now()
.checked_add_months(chrono::Months::new(1))
.unwrap()
.with_day(1)
.unwrap();
let mut params = stripe::CreateCheckoutSession::new();
params.mode = Some(stripe::CheckoutSessionMode::Subscription);
params.customer = Some(customer_id);
params.client_reference_id = Some(github_login);
params.subscription_data = Some(stripe::CreateCheckoutSessionSubscriptionData {
billing_cycle_anchor: Some(first_of_next_month.timestamp()),
..Default::default()
});
params.line_items = Some(
[
&model.input_tokens_price.id,

View File

@@ -48,6 +48,7 @@ mod signature_help;
pub mod test;
use ::git::diff::DiffHunkStatus;
use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
pub(crate) use actions::*;
use aho_corasick::AhoCorasick;
use anyhow::{anyhow, Context as _, Result};
@@ -100,7 +101,7 @@ use language::{
};
use linked_editing_ranges::refresh_linked_ranges;
pub use proposed_changes_editor::{
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
ProposedChangesBuffer, ProposedChangesEditor, ProposedChangesEditorToolbar,
};
use similar::{ChangeTag, TextDiff};
use task::{ResolvedTask, TaskTemplate, TaskVariables};
@@ -11487,8 +11488,11 @@ impl Editor {
snapshot.line_len(buffer_row) == 0
}
fn get_permalink_to_line(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<url::Url>> {
let buffer_and_selection = maybe!({
fn get_permalink_to_line(&mut self, cx: &mut ViewContext<Self>) -> Result<url::Url> {
let (path, selection, repo) = maybe!({
let project_handle = self.project.as_ref()?.clone();
let project = project_handle.read(cx);
let selection = self.selections.newest::<Point>(cx);
let selection_range = selection.range();
@@ -11512,58 +11516,64 @@ impl Editor {
(buffer.clone(), selection)
};
Some((buffer, selection))
});
let Some((buffer, selection)) = buffer_and_selection else {
return Task::ready(Err(anyhow!("failed to determine buffer and selection")));
};
let Some(project) = self.project.as_ref() else {
return Task::ready(Err(anyhow!("editor does not have project")));
};
project.update(cx, |project, cx| {
project.get_permalink_to_line(&buffer, selection, cx)
let path = buffer
.read(cx)
.file()?
.as_local()?
.path()
.to_str()?
.to_string();
let repo = project.get_repo(&buffer.read(cx).project_path(cx)?, cx)?;
Some((path, selection, repo))
})
.ok_or_else(|| anyhow!("unable to open git repository"))?;
const REMOTE_NAME: &str = "origin";
let origin_url = repo
.remote_url(REMOTE_NAME)
.ok_or_else(|| anyhow!("remote \"{REMOTE_NAME}\" not found"))?;
let sha = repo
.head_sha()
.ok_or_else(|| anyhow!("failed to read HEAD SHA"))?;
let (provider, remote) =
parse_git_remote_url(GitHostingProviderRegistry::default_global(cx), &origin_url)
.ok_or_else(|| anyhow!("failed to parse Git remote URL"))?;
Ok(provider.build_permalink(
remote,
BuildPermalinkParams {
sha: &sha,
path: &path,
selection: Some(selection),
},
))
}
pub fn copy_permalink_to_line(&mut self, _: &CopyPermalinkToLine, cx: &mut ViewContext<Self>) {
let permalink_task = self.get_permalink_to_line(cx);
let workspace = self.workspace();
let permalink = self.get_permalink_to_line(cx);
cx.spawn(|_, mut cx| async move {
match permalink_task.await {
Ok(permalink) => {
cx.update(|cx| {
cx.write_to_clipboard(ClipboardItem::new_string(permalink.to_string()));
match permalink {
Ok(permalink) => {
cx.write_to_clipboard(ClipboardItem::new_string(permalink.to_string()));
}
Err(err) => {
let message = format!("Failed to copy permalink: {err}");
Err::<(), anyhow::Error>(err).log_err();
if let Some(workspace) = self.workspace() {
workspace.update(cx, |workspace, cx| {
struct CopyPermalinkToLine;
workspace.show_toast(
Toast::new(NotificationId::unique::<CopyPermalinkToLine>(), message),
cx,
)
})
.ok();
}
Err(err) => {
let message = format!("Failed to copy permalink: {err}");
Err::<(), anyhow::Error>(err).log_err();
if let Some(workspace) = workspace {
workspace
.update(&mut cx, |workspace, cx| {
struct CopyPermalinkToLine;
workspace.show_toast(
Toast::new(
NotificationId::unique::<CopyPermalinkToLine>(),
message,
),
cx,
)
})
.ok();
}
}
}
})
.detach();
}
}
pub fn copy_file_location(&mut self, _: &CopyFileLocation, cx: &mut ViewContext<Self>) {
@@ -11576,41 +11586,29 @@ impl Editor {
}
pub fn open_permalink_to_line(&mut self, _: &OpenPermalinkToLine, cx: &mut ViewContext<Self>) {
let permalink_task = self.get_permalink_to_line(cx);
let workspace = self.workspace();
let permalink = self.get_permalink_to_line(cx);
cx.spawn(|_, mut cx| async move {
match permalink_task.await {
Ok(permalink) => {
cx.update(|cx| {
cx.open_url(permalink.as_ref());
match permalink {
Ok(permalink) => {
cx.open_url(permalink.as_ref());
}
Err(err) => {
let message = format!("Failed to open permalink: {err}");
Err::<(), anyhow::Error>(err).log_err();
if let Some(workspace) = self.workspace() {
workspace.update(cx, |workspace, cx| {
struct OpenPermalinkToLine;
workspace.show_toast(
Toast::new(NotificationId::unique::<OpenPermalinkToLine>(), message),
cx,
)
})
.ok();
}
Err(err) => {
let message = format!("Failed to open permalink: {err}");
Err::<(), anyhow::Error>(err).log_err();
if let Some(workspace) = workspace {
workspace
.update(&mut cx, |workspace, cx| {
struct OpenPermalinkToLine;
workspace.show_toast(
Toast::new(
NotificationId::unique::<OpenPermalinkToLine>(),
message,
),
cx,
)
})
.ok();
}
}
}
})
.detach();
}
}
/// Adds a row highlight for the given range. If a row has multiple highlights, the
@@ -12363,15 +12361,10 @@ impl Editor {
let proposed_changes_buffers = new_selections_by_buffer
.into_iter()
.map(|(buffer, ranges)| ProposedChangeLocation { buffer, ranges })
.map(|(buffer, ranges)| ProposedChangesBuffer { buffer, ranges })
.collect::<Vec<_>>();
let proposed_changes_editor = cx.new_view(|cx| {
ProposedChangesEditor::new(
"Proposed changes",
proposed_changes_buffers,
self.project.clone(),
cx,
)
ProposedChangesEditor::new(proposed_changes_buffers, self.project.clone(), cx)
});
cx.window_context().defer(move |cx| {

View File

@@ -16,24 +16,16 @@ use workspace::{
pub struct ProposedChangesEditor {
editor: View<Editor>,
multibuffer: Model<MultiBuffer>,
title: SharedString,
buffer_entries: Vec<BufferEntry>,
_subscriptions: Vec<Subscription>,
_recalculate_diffs_task: Task<Option<()>>,
recalculate_diffs_tx: mpsc::UnboundedSender<RecalculateDiff>,
}
pub struct ProposedChangeLocation<T> {
pub struct ProposedChangesBuffer<T> {
pub buffer: Model<Buffer>,
pub ranges: Vec<Range<T>>,
}
struct BufferEntry {
base: Model<Buffer>,
branch: Model<Buffer>,
_subscription: Subscription,
}
pub struct ProposedChangesEditorToolbar {
current_editor: Option<View<ProposedChangesEditor>>,
}
@@ -51,14 +43,32 @@ struct BranchBufferSemanticsProvider(Rc<dyn SemanticsProvider>);
impl ProposedChangesEditor {
pub fn new<T: ToOffset>(
title: impl Into<SharedString>,
locations: Vec<ProposedChangeLocation<T>>,
buffers: Vec<ProposedChangesBuffer<T>>,
project: Option<Model<Project>>,
cx: &mut ViewContext<Self>,
) -> Self {
let mut subscriptions = Vec::new();
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 mut this = Self {
Self {
editor: cx.new_view(|cx| {
let mut editor = Editor::for_multibuffer(multibuffer.clone(), project, true, cx);
editor.set_expand_all_diff_hunks();
@@ -71,9 +81,6 @@ impl ProposedChangesEditor {
);
editor
}),
multibuffer,
title: title.into(),
buffer_entries: Vec::new(),
recalculate_diffs_tx,
_recalculate_diffs_task: cx.spawn(|_, mut cx| async move {
let mut buffers_to_diff = HashSet::default();
@@ -105,100 +112,7 @@ impl ProposedChangesEditor {
}
None
}),
};
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();
_subscriptions: subscriptions,
}
}
@@ -248,11 +162,11 @@ impl Item for ProposedChangesEditor {
type Event = EditorEvent;
fn tab_icon(&self, _cx: &ui::WindowContext) -> Option<Icon> {
Some(Icon::new(IconName::Diff))
Some(Icon::new(IconName::Pencil))
}
fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
Some(self.title.clone())
Some("Proposed changes".into())
}
fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {

View File

@@ -434,10 +434,12 @@ impl<T> Clone for Model<T> {
impl<T> std::fmt::Debug for Model<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Model")
.field("entity_id", &self.any_model.entity_id)
.field("entity_type", &type_name::<T>())
.finish()
write!(
f,
"Model {{ entity_id: {:?}, entity_type: {:?} }}",
self.any_model.entity_id,
type_name::<T>()
)
}
}
@@ -567,10 +569,7 @@ pub struct WeakModel<T> {
impl<T> std::fmt::Debug for WeakModel<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct(&type_name::<Self>())
.field("entity_id", &self.any_model.entity_id)
.field("entity_type", &type_name::<T>())
.finish()
f.debug_struct(type_name::<WeakModel<T>>()).finish()
}
}

View File

@@ -36,9 +36,7 @@ impl project::Item for ImageItem {
.path
.extension()
.and_then(OsStr::to_str)
.map(str::to_lowercase)
.unwrap_or_default();
let ext = ext.as_str();
// Only open the item if it's a binary image (no SVGs, etc.)
// Since we do not have a way to toggle to an editor

View File

@@ -20,7 +20,6 @@ use anyhow::{anyhow, Context, Result};
use async_watch as watch;
use clock::Lamport;
pub use clock::ReplicaId;
use collections::HashMap;
use futures::channel::oneshot;
use gpui::{
AnyElement, AppContext, Context as _, EventEmitter, HighlightStyle, Model, ModelContext,
@@ -911,8 +910,10 @@ impl Buffer {
self.apply_ops([operation.clone()], cx);
if let Some(timestamp) = operation_to_undo {
let counts = [(timestamp, u32::MAX)].into_iter().collect();
self.undo_operations(counts, cx);
let operation = self
.text
.undo_operations([(timestamp, u32::MAX)].into_iter().collect());
self.send_operation(Operation::Buffer(operation), true, cx);
}
self.diff_base_version += 1;
@@ -2330,18 +2331,6 @@ impl Buffer {
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.
pub fn redo(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
let was_dirty = self.is_dirty();

View File

@@ -9,9 +9,7 @@ license = "GPL-3.0-or-later"
workspace = true
[features]
test-support = [
"tree-sitter"
]
test-support = []
load-grammars = [
"tree-sitter-bash",
"tree-sitter-c",
@@ -77,7 +75,6 @@ tree-sitter-yaml = { workspace = true, optional = true }
util.workspace = true
[dev-dependencies]
tree-sitter.workspace = true
text.workspace = true
theme = { workspace = true, features = ["test-support"] }
unindent.workspace = true

View File

@@ -101,10 +101,10 @@ pub fn logs_dir() -> &'static PathBuf {
})
}
/// Returns the path to the Zed server directory on this SSH host.
/// Returns the path to the zed server directory on this ssh host.
pub fn remote_server_state_dir() -> &'static PathBuf {
static REMOTE_SERVER_STATE: OnceLock<PathBuf> = OnceLock::new();
REMOTE_SERVER_STATE.get_or_init(|| support_dir().join("server_state"))
REMOTE_SERVER_STATE.get_or_init(|| return support_dir().join("server_state"))
}
/// Returns the path to the `Zed.log` file.

View File

@@ -69,7 +69,6 @@ snippet_provider.workspace = true
terminal.workspace = true
text.workspace = true
util.workspace = true
url.workspace = true
which.workspace = true
[target.'cfg(target_os = "windows")'.dependencies]

View File

@@ -3,7 +3,6 @@ use crate::{
worktree_store::{WorktreeStore, WorktreeStoreEvent},
Item, NoRepositoryError, ProjectPath,
};
use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
use anyhow::{anyhow, Context as _, Result};
use client::Client;
use collections::{hash_map, HashMap, HashSet};
@@ -24,7 +23,7 @@ use language::{
};
use rpc::{proto, AnyProtoClient, ErrorExt as _, TypedEnvelope};
use smol::channel::Receiver;
use std::{io, ops::Range, path::Path, str::FromStr as _, sync::Arc, time::Instant};
use std::{io, path::Path, str::FromStr as _, sync::Arc, time::Instant};
use text::BufferId;
use util::{debug_panic, maybe, ResultExt as _, TryFutureExt};
use worktree::{File, PathChange, ProjectEntryId, UpdatedGitRepositoriesSet, Worktree, WorktreeId};
@@ -972,7 +971,6 @@ impl BufferStore {
client.add_model_request_handler(Self::handle_save_buffer);
client.add_model_request_handler(Self::handle_blame_buffer);
client.add_model_request_handler(Self::handle_reload_buffers);
client.add_model_request_handler(Self::handle_get_permalink_to_line);
}
/// Creates a buffer store, optionally retaining its buffers.
@@ -1172,78 +1170,6 @@ impl BufferStore {
}
}
pub fn get_permalink_to_line(
&self,
buffer: &Model<Buffer>,
selection: Range<u32>,
cx: &AppContext,
) -> Task<Result<url::Url>> {
let buffer = buffer.read(cx);
let Some(file) = File::from_dyn(buffer.file()) else {
return Task::ready(Err(anyhow!("buffer has no file")));
};
match file.worktree.clone().read(cx) {
Worktree::Local(worktree) => {
let Some(repo) = worktree.local_git_repo(file.path()) else {
return Task::ready(Err(anyhow!("no repository for buffer found")));
};
let path = file.path().clone();
cx.spawn(|cx| async move {
const REMOTE_NAME: &str = "origin";
let origin_url = repo
.remote_url(REMOTE_NAME)
.ok_or_else(|| anyhow!("remote \"{REMOTE_NAME}\" not found"))?;
let sha = repo
.head_sha()
.ok_or_else(|| anyhow!("failed to read HEAD SHA"))?;
let provider_registry =
cx.update(GitHostingProviderRegistry::default_global)?;
let (provider, remote) =
parse_git_remote_url(provider_registry, &origin_url)
.ok_or_else(|| anyhow!("failed to parse Git remote URL"))?;
let path = path
.to_str()
.context("failed to convert buffer path to string")?;
Ok(provider.build_permalink(
remote,
BuildPermalinkParams {
sha: &sha,
path,
selection: Some(selection),
},
))
})
}
Worktree::Remote(worktree) => {
let buffer_id = buffer.remote_id();
let project_id = worktree.project_id();
let client = worktree.client();
cx.spawn(|_| async move {
let response = client
.request(proto::GetPermalinkToLine {
project_id,
buffer_id: buffer_id.into(),
selection: Some(proto::Range {
start: selection.start as u64,
end: selection.end as u64,
}),
})
.await?;
url::Url::parse(&response.permalink).context("failed to parse permalink")
})
}
}
}
fn add_buffer(&mut self, buffer: Model<Buffer>, cx: &mut ModelContext<Self>) -> Result<()> {
let remote_id = buffer.read(cx).remote_id();
let is_remote = buffer.read(cx).replica_id() != 0;
@@ -1849,31 +1775,6 @@ impl BufferStore {
Ok(serialize_blame_buffer_response(blame))
}
pub async fn handle_get_permalink_to_line(
this: Model<Self>,
envelope: TypedEnvelope<proto::GetPermalinkToLine>,
mut cx: AsyncAppContext,
) -> Result<proto::GetPermalinkToLineResponse> {
let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
// let version = deserialize_version(&envelope.payload.version);
let selection = {
let proto_selection = envelope
.payload
.selection
.context("no selection to get permalink for defined")?;
proto_selection.start as u32..proto_selection.end as u32
};
let buffer = this.read_with(&cx, |this, _| this.get_existing(buffer_id))??;
let permalink = this
.update(&mut cx, |this, cx| {
this.get_permalink_to_line(&buffer, selection, cx)
})?
.await?;
Ok(proto::GetPermalinkToLineResponse {
permalink: permalink.to_string(),
})
}
pub async fn wait_for_loading_buffer(
mut receiver: postage::watch::Receiver<Option<Result<Model<Buffer>, Arc<anyhow::Error>>>>,
) -> Result<Model<Buffer>, Arc<anyhow::Error>> {

View File

@@ -3463,17 +3463,6 @@ impl Project {
self.buffer_store.read(cx).blame_buffer(buffer, version, cx)
}
pub fn get_permalink_to_line(
&self,
buffer: &Model<Buffer>,
selection: Range<u32>,
cx: &AppContext,
) -> Task<Result<url::Url>> {
self.buffer_store
.read(cx)
.get_permalink_to_line(buffer, selection, cx)
}
// RPC message handlers
async fn handle_unshare_project(

View File

@@ -292,10 +292,7 @@ message Envelope {
Toast toast = 261;
HideToast hide_toast = 262;
OpenServerSettings open_server_settings = 263;
GetPermalinkToLine get_permalink_to_line = 264;
GetPermalinkToLineResponse get_permalink_to_line_response = 265; // current max
OpenServerSettings open_server_settings = 263; // current max
}
reserved 87 to 88;
@@ -2511,13 +2508,3 @@ message HideToast {
message OpenServerSettings {
uint64 project_id = 1;
}
message GetPermalinkToLine {
uint64 project_id = 1;
uint64 buffer_id = 2;
Range selection = 3;
}
message GetPermalinkToLineResponse {
string permalink = 1;
}

View File

@@ -370,8 +370,6 @@ messages!(
(Toast, Background),
(HideToast, Background),
(OpenServerSettings, Foreground),
(GetPermalinkToLine, Foreground),
(GetPermalinkToLineResponse, Foreground),
);
request_messages!(
@@ -496,8 +494,7 @@ request_messages!(
(CheckFileExists, CheckFileExistsResponse),
(ShutdownRemoteServer, Ack),
(RemoveWorktree, Ack),
(OpenServerSettings, OpenBufferResponse),
(GetPermalinkToLine, GetPermalinkToLineResponse),
(OpenServerSettings, OpenBufferResponse)
);
entity_messages!(
@@ -574,7 +571,7 @@ entity_messages!(
Toast,
HideToast,
OpenServerSettings,
GetPermalinkToLine,
);
entity_messages!(

View File

@@ -22,7 +22,6 @@ file_finder.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
itertools.workspace = true
log.workspace = true
menu.workspace = true
ordered-float.workspace = true

View File

@@ -13,7 +13,6 @@ use gpui::{
Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
Subscription, Task, View, ViewContext, WeakView,
};
use itertools::Itertools;
use ordered_float::OrderedFloat;
use picker::{
highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText},
@@ -248,9 +247,8 @@ impl PickerDelegate for RecentProjectsDelegate {
SerializedWorkspaceLocation::Local(paths, order) => order
.order()
.iter()
.zip(paths.paths().iter())
.sorted_by_key(|(i, _)| *i)
.map(|(_, path)| path.compact().to_string_lossy().into_owned())
.filter_map(|i| paths.paths().get(*i))
.map(|path| path.compact().to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join(""),
SerializedWorkspaceLocation::DevServer(dev_server_project) => {
@@ -449,9 +447,8 @@ impl PickerDelegate for RecentProjectsDelegate {
order
.order()
.iter()
.zip(paths.paths().iter())
.sorted_by_key(|(i, _)| **i)
.map(|(_, path)| path.compact())
.filter_map(|i| paths.paths().get(*i).cloned())
.map(|path| path.compact())
.collect(),
),
SerializedWorkspaceLocation::Ssh(ssh_project) => Arc::new(ssh_project.ssh_urls()),

View File

@@ -832,15 +832,7 @@ impl SshRemoteClient {
log::warn!("ssh heartbeat: connection activity channel has been dropped. stopping.");
return Ok(());
}
keepalive_timer.set(cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse());
if missed_heartbeats != 0 {
missed_heartbeats = 0;
this.update(&mut cx, |this, mut cx| {
this.handle_heartbeat_result(missed_heartbeats, &mut cx)
})?;
}
}
_ = keepalive_timer => {
log::debug!("Sending heartbeat to server...");
@@ -853,7 +845,6 @@ impl SshRemoteClient {
ping_result
}
};
if result.is_err() {
missed_heartbeats += 1;
log::warn!(

View File

@@ -30,8 +30,6 @@ client.workspace = true
env_logger.workspace = true
fs.workspace = true
futures.workspace = true
git.workspace = true
git_hosting_providers.workspace = true
gpui.workspace = true
http_client.workspace = true
language.workspace = true

View File

@@ -154,7 +154,6 @@ impl HeadlessProject {
client.add_request_handler(cx.weak_model(), Self::handle_remove_worktree);
client.add_model_request_handler(Self::handle_open_buffer_by_path);
client.add_model_request_handler(Self::handle_open_new_buffer);
client.add_model_request_handler(Self::handle_find_search_candidates);
client.add_model_request_handler(Self::handle_open_server_settings);
@@ -364,32 +363,6 @@ impl HeadlessProject {
})
}
pub async fn handle_open_new_buffer(
this: Model<Self>,
_message: TypedEnvelope<proto::OpenNewBuffer>,
mut cx: AsyncAppContext,
) -> Result<proto::OpenBufferResponse> {
let (buffer_store, buffer) = this.update(&mut cx, |this, cx| {
let buffer_store = this.buffer_store.clone();
let buffer = this
.buffer_store
.update(cx, |buffer_store, cx| buffer_store.create_buffer(cx));
anyhow::Ok((buffer_store, buffer))
})??;
let buffer = buffer.await?;
let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?;
buffer_store.update(&mut cx, |buffer_store, cx| {
buffer_store
.create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
.detach_and_log_err(cx);
})?;
Ok(proto::OpenBufferResponse {
buffer_id: buffer_id.to_proto(),
})
}
pub async fn handle_open_server_settings(
this: Model<Self>,
_: TypedEnvelope<proto::OpenServerSettings>,

View File

@@ -5,7 +5,6 @@ use client::ProxySettings;
use fs::{Fs, RealFs};
use futures::channel::mpsc;
use futures::{select, select_biased, AsyncRead, AsyncWrite, AsyncWriteExt, FutureExt, SinkExt};
use git::GitHostingProviderRegistry;
use gpui::{AppContext, Context as _, ModelContext, UpdateGlobal as _};
use http_client::{read_proxy_from_env, Uri};
use language::LanguageRegistry;
@@ -314,8 +313,6 @@ pub fn execute_run(
let listeners = ServerListeners::new(stdin_socket, stdout_socket, stderr_socket)?;
log::info!("starting headless gpui app");
let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new());
gpui::App::headless().run(move |cx| {
settings::init(cx);
HeadlessProject::init(cx);
@@ -325,9 +322,6 @@ pub fn execute_run(
client::init_settings(cx);
GitHostingProviderRegistry::set_global(git_hosting_provider_registry, cx);
git_hosting_providers::init(cx);
let project = cx.new_model(|cx| {
let fs = Arc::new(RealFs::new(Default::default(), None));
let node_settings_rx = initialize_settings(session.clone(), fs.clone(), cx);

View File

@@ -151,7 +151,7 @@ pub struct TerminalSettingsContent {
pub alternate_scroll: Option<AlternateScroll>,
/// Sets whether the option key behaves as the meta key.
///
/// Default: false
/// Default: true
pub option_as_meta: Option<bool>,
/// Whether or not selecting text in the terminal will automatically
/// copy to the system clipboard.

View File

@@ -1427,7 +1427,7 @@ impl Buffer {
fn undo_or_redo(&mut self, transaction: Transaction) -> Operation {
let mut counts = HashMap::default();
for edit_id in transaction.edit_ids {
counts.insert(edit_id, self.undo_map.undo_count(edit_id).saturating_add(1));
counts.insert(edit_id, self.undo_map.undo_count(edit_id) + 1);
}
let operation = self.undo_operations(counts);

View File

@@ -170,7 +170,6 @@ pub enum IconName {
Dash,
DatabaseZap,
Delete,
Diff,
Disconnected,
Download,
Ellipsis,

View File

@@ -380,8 +380,6 @@ impl WorkspaceDb {
&self,
worktree_roots: &[P],
) -> Option<SerializedWorkspace> {
// paths are sorted before db interactions to ensure that the order of the paths
// doesn't affect the workspace selection for existing workspaces
let local_paths = LocalPaths::new(worktree_roots);
// Note that we re-assign the workspace_id here in case it's empty
@@ -835,8 +833,8 @@ impl WorkspaceDb {
}
query! {
fn session_workspaces(session_id: String) -> Result<Vec<(LocalPaths, LocalPathsOrder, Option<u64>, Option<u64>)>> {
SELECT local_paths, local_paths_order, window_id, ssh_project_id
fn session_workspaces(session_id: String) -> Result<Vec<(LocalPaths, Option<u64>, Option<u64>)>> {
SELECT local_paths, window_id, ssh_project_id
FROM workspaces
WHERE session_id = ?1 AND dev_server_project_id IS NULL
ORDER BY timestamp DESC
@@ -973,7 +971,7 @@ impl WorkspaceDb {
) -> Result<Vec<SerializedWorkspaceLocation>> {
let mut workspaces = Vec::new();
for (location, order, window_id, ssh_project_id) in
for (location, window_id, ssh_project_id) in
self.session_workspaces(last_session_id.to_owned())?
{
if let Some(ssh_project_id) = ssh_project_id {
@@ -982,7 +980,8 @@ impl WorkspaceDb {
} else if location.paths().iter().all(|path| path.exists())
&& location.paths().iter().any(|path| path.is_dir())
{
let location = SerializedWorkspaceLocation::Local(location, order);
let location =
SerializedWorkspaceLocation::from_local_paths(location.paths().iter());
workspaces.push((location, window_id.map(WindowId::from)));
}
}
@@ -1604,56 +1603,27 @@ mod tests {
window_id: Some(50),
};
let workspace_6 = SerializedWorkspace {
id: WorkspaceId(6),
location: SerializedWorkspaceLocation::Local(
LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]),
LocalPathsOrder::new([2, 1, 0]),
),
center_group: Default::default(),
window_bounds: Default::default(),
display: Default::default(),
docks: Default::default(),
centered_layout: false,
session_id: Some("session-id-3".to_owned()),
window_id: Some(60),
};
db.save_workspace(workspace_1.clone()).await;
db.save_workspace(workspace_2.clone()).await;
db.save_workspace(workspace_3.clone()).await;
db.save_workspace(workspace_4.clone()).await;
db.save_workspace(workspace_5.clone()).await;
db.save_workspace(workspace_6.clone()).await;
let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
assert_eq!(locations.len(), 2);
assert_eq!(locations[0].0, LocalPaths::new(["/tmp1"]));
assert_eq!(locations[0].1, LocalPathsOrder::new([0]));
assert_eq!(locations[0].2, Some(10));
assert_eq!(locations[0].1, Some(10));
assert_eq!(locations[1].0, LocalPaths::new(["/tmp2"]));
assert_eq!(locations[1].1, LocalPathsOrder::new([0]));
assert_eq!(locations[1].2, Some(20));
assert_eq!(locations[1].1, Some(20));
let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
assert_eq!(locations.len(), 2);
assert_eq!(locations[0].0, LocalPaths::new(["/tmp3"]));
assert_eq!(locations[0].1, LocalPathsOrder::new([0]));
assert_eq!(locations[0].2, Some(30));
assert_eq!(locations[0].1, Some(30));
let empty_paths: Vec<&str> = Vec::new();
assert_eq!(locations[1].0, LocalPaths::new(empty_paths.iter()));
assert_eq!(locations[1].1, LocalPathsOrder::new([]));
assert_eq!(locations[1].2, Some(50));
assert_eq!(locations[1].3, Some(ssh_project.id.0));
let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
assert_eq!(locations.len(), 1);
assert_eq!(
locations[0].0,
LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]),
);
assert_eq!(locations[0].1, LocalPathsOrder::new([2, 1, 0]));
assert_eq!(locations[0].2, Some(60));
assert_eq!(locations[1].1, Some(50));
assert_eq!(locations[1].2, Some(ssh_project.id.0));
}
fn default_workspace<P: AsRef<Path>>(
@@ -1684,30 +1654,15 @@ mod tests {
WorkspaceDb(open_test_db("test_serializing_workspaces_last_session_workspaces").await);
let workspaces = [
(1, vec![dir1.path()], vec![0], 9),
(2, vec![dir2.path()], vec![0], 5),
(3, vec![dir3.path()], vec![0], 8),
(4, vec![dir4.path()], vec![0], 2),
(
5,
vec![dir1.path(), dir2.path(), dir3.path()],
vec![0, 1, 2],
3,
),
(
6,
vec![dir2.path(), dir3.path(), dir4.path()],
vec![2, 1, 0],
4,
),
(1, dir1.path().to_str().unwrap(), 9),
(2, dir2.path().to_str().unwrap(), 5),
(3, dir3.path().to_str().unwrap(), 8),
(4, dir4.path().to_str().unwrap(), 2),
]
.into_iter()
.map(|(id, locations, order, window_id)| SerializedWorkspace {
.map(|(id, location, window_id)| SerializedWorkspace {
id: WorkspaceId(id),
location: SerializedWorkspaceLocation::Local(
LocalPaths::new(locations),
LocalPathsOrder::new(order),
),
location: SerializedWorkspaceLocation::from_local_paths([location]),
center_group: Default::default(),
window_bounds: Default::default(),
display: Default::default(),
@@ -1726,44 +1681,28 @@ mod tests {
WindowId::from(2), // Top
WindowId::from(8),
WindowId::from(5),
WindowId::from(9),
WindowId::from(3),
WindowId::from(4), // Bottom
WindowId::from(9), // Bottom
]));
let have = db
.last_session_workspace_locations("one-session", stack)
.unwrap();
assert_eq!(have.len(), 6);
assert_eq!(have.len(), 4);
assert_eq!(
have[0],
SerializedWorkspaceLocation::from_local_paths(&[dir4.path()])
SerializedWorkspaceLocation::from_local_paths(&[dir4.path().to_str().unwrap()])
);
assert_eq!(
have[1],
SerializedWorkspaceLocation::from_local_paths([dir3.path()])
SerializedWorkspaceLocation::from_local_paths([dir3.path().to_str().unwrap()])
);
assert_eq!(
have[2],
SerializedWorkspaceLocation::from_local_paths([dir2.path()])
SerializedWorkspaceLocation::from_local_paths([dir2.path().to_str().unwrap()])
);
assert_eq!(
have[3],
SerializedWorkspaceLocation::from_local_paths([dir1.path()])
);
assert_eq!(
have[4],
SerializedWorkspaceLocation::Local(
LocalPaths::new([dir1.path(), dir2.path(), dir3.path()]),
LocalPathsOrder::new([0, 1, 2]),
),
);
assert_eq!(
have[5],
SerializedWorkspaceLocation::Local(
LocalPaths::new([dir2.path(), dir3.path(), dir4.path()]),
LocalPathsOrder::new([2, 1, 0]),
),
SerializedWorkspaceLocation::from_local_paths([dir1.path().to_str().unwrap()])
);
}

View File

@@ -1103,28 +1103,20 @@ impl Workspace {
let mut paths_to_open = abs_paths;
let workspace_location = serialized_workspace
let paths_order = serialized_workspace
.as_ref()
.map(|ws| &ws.location)
.and_then(|loc| match loc {
SerializedWorkspaceLocation::Local(paths, order) => {
Some((paths.paths(), order.order()))
}
SerializedWorkspaceLocation::Local(_, order) => Some(order.order()),
_ => None,
});
if let Some((paths, order)) = workspace_location {
// todo: should probably move this logic to a method on the SerializedWorkspaceLocation
// it's only valid for Local and would be more clear there and be able to be tested
// and reused elsewhere
paths_to_open = order
if let Some(paths_order) = paths_order {
paths_to_open = paths_order
.iter()
.zip(paths.iter())
.sorted_by_key(|(i, _)| *i)
.map(|(_, path)| path.clone())
.collect();
if order.iter().enumerate().any(|(i, &j)| i != j) {
.filter_map(|i| paths_to_open.get(*i).cloned())
.collect::<Vec<_>>();
if paths_order.iter().enumerate().any(|(i, &j)| i != j) {
project_handle
.update(&mut cx, |project, cx| {
project.set_worktrees_reordered(true, cx);
@@ -1897,7 +1889,11 @@ impl Workspace {
directories: true,
multiple: true,
},
DirectoryLister::Local(self.app_state.fs.clone()),
if self.project.read(cx).is_via_ssh() {
DirectoryLister::Project(self.project.clone())
} else {
DirectoryLister::Local(self.app_state.fs.clone())
},
cx,
);

View File

@@ -1498,13 +1498,13 @@ List of `integer` column numbers
"directories": [".env", "env", ".venv", "venv"],
"activate_script": "default"
}
},
}
"env": {},
"font_family": null,
"font_features": null,
"font_size": null,
"line_height": "comfortable",
"option_as_meta": false,
"option_as_meta": true,
"button": false,
"shell": {},
"toolbar": {
@@ -1732,7 +1732,7 @@ See Buffer Font Features
- Description: Re-interprets the option keys to act like a 'meta' key, like in Emacs.
- Setting: `option_as_meta`
- Default: `false`
- Default: `true`
**Options**

14
script/check-spelling Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/sh
set -eu
TYPOS_CLI_VERSION=1.24.6
TARGET_DIR=${1:-""}
if ! cargo install --list | grep "typos-cli v$TYPOS_CLI_VERSION" > /dev/null; then
echo "Installing typos-cli@$TYPOS_CLI_VERSION..."
cargo install "typos-cli@$TYPOS_CLI_VERSION"
else
echo "typos-cli@$TYPOS_CLI_VERSION is already installed."
fi
typos $TARGET_DIR