Compare commits

..

3 Commits

Author SHA1 Message Date
Richard Feldman
9404251dd0 Tweak workflow prompt to be more explicit about step boundaries 2024-08-16 16:41:15 -04:00
Richard Feldman
289ba8fb34 Make find_most_similar resilient to queries that contain types 2024-08-16 16:14:53 -04:00
Richard Feldman
ba7e894a6e Simplify workflow prompt 2024-08-16 15:41:47 -04:00
200 changed files with 5840 additions and 8889 deletions

View File

@@ -3,15 +3,6 @@ export default {
const url = new URL(request.url);
url.hostname = "docs-anw.pages.dev";
// These pages were removed, but may still be served due to Cloudflare's
// [asset retention](https://developers.cloudflare.com/pages/configuration/serving-pages/#asset-retention).
if (
url.pathname === "/docs/assistant/context-servers" ||
url.pathname === "/docs/assistant/model-context-protocol"
) {
return await fetch("https://zed.dev/404");
}
let res = await fetch(url, request);
if (res.status === 404) {

View File

@@ -167,7 +167,6 @@ jobs:
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
steps:
@@ -277,7 +276,6 @@ jobs:
needs: [linux_tests]
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
@@ -348,7 +346,6 @@ jobs:
needs: [linux_tests]
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4

View File

@@ -67,7 +67,6 @@ jobs:
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
steps:
- name: Install Node
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4
@@ -107,7 +106,6 @@ jobs:
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
@@ -141,7 +139,6 @@ jobs:
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
steps:
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4

57
Cargo.lock generated
View File

@@ -223,7 +223,6 @@ name = "anthropic"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"futures 0.3.30",
"http_client",
"isahc",
@@ -233,7 +232,6 @@ dependencies = [
"strum",
"thiserror",
"tokio",
"util",
]
[[package]]
@@ -2329,6 +2327,7 @@ dependencies = [
"futures 0.3.30",
"gpui",
"http_client",
"lazy_static",
"log",
"once_cell",
"parking_lot",
@@ -2530,6 +2529,7 @@ dependencies = [
"gpui",
"http_client",
"language",
"lazy_static",
"menu",
"notifications",
"parking_lot",
@@ -3261,6 +3261,7 @@ dependencies = [
"anyhow",
"gpui",
"indoc",
"lazy_static",
"log",
"paths",
"release_channel",
@@ -3540,6 +3541,7 @@ dependencies = [
"indoc",
"itertools 0.11.0",
"language",
"lazy_static",
"linkify",
"log",
"lsp",
@@ -4302,6 +4304,7 @@ dependencies = [
"git",
"git2",
"gpui",
"lazy_static",
"libc",
"notify",
"objc",
@@ -4627,6 +4630,7 @@ dependencies = [
"git2",
"gpui",
"http_client",
"lazy_static",
"log",
"parking_lot",
"pretty_assertions",
@@ -4825,6 +4829,7 @@ dependencies = [
"http_client",
"image",
"itertools 0.11.0",
"lazy_static",
"linkme",
"log",
"media",
@@ -5052,9 +5057,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "heed"
version = "0.20.5"
version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d4f449bab7320c56003d37732a917e18798e2f1709d80263face2b4f9436ddb"
checksum = "620033c8c8edfd2f53e6f99a30565eb56a33b42c468e3ad80e21d85fb93bafb0"
dependencies = [
"bitflags 2.6.0",
"byteorder",
@@ -5940,6 +5945,7 @@ dependencies = [
"http_client",
"indoc",
"itertools 0.11.0",
"lazy_static",
"log",
"lsp",
"parking_lot",
@@ -6073,6 +6079,7 @@ dependencies = [
"gpui",
"http_client",
"language",
"lazy_static",
"log",
"lsp",
"node_runtime",
@@ -6307,9 +6314,9 @@ dependencies = [
[[package]]
name = "lmdb-master-sys"
version = "0.2.4"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "472c3760e2a8d0f61f322fb36788021bb36d573c502b50fa3e2bcaac3ec326c9"
checksum = "1de7e761853c15ca72821d9f928d7bb123ef4c05377c4e7ab69fa1c742f91d24"
dependencies = [
"cc",
"doxygen-rs",
@@ -7583,29 +7590,6 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "performance"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"gpui",
"log",
"schemars",
"serde",
"settings",
"util",
"workspace",
]
[[package]]
name = "perplexity"
version = "0.1.0"
dependencies = [
"serde",
"zed_extension_api 0.1.0",
]
[[package]]
name = "pest"
version = "2.7.11"
@@ -9049,9 +9033,9 @@ dependencies = [
[[package]]
name = "runtimelib"
version = "0.15.0"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7d76d28b882a7b889ebb04e79bc2b160b3061821ea596ff0f4a838fc7a76db0"
checksum = "0c3d817764e3971867351e6103955b17d808f5330e9ef63aaaaab55bf8c664c1"
dependencies = [
"anyhow",
"async-dispatcher",
@@ -9732,6 +9716,7 @@ dependencies = [
"futures 0.3.30",
"gpui",
"indoc",
"lazy_static",
"log",
"paths",
"pretty_assertions",
@@ -10143,6 +10128,7 @@ dependencies = [
"collections",
"futures 0.3.30",
"indoc",
"lazy_static",
"libsqlite3-sys",
"parking_lot",
"smol",
@@ -10155,6 +10141,7 @@ dependencies = [
name = "sqlez_macros"
version = "0.1.0"
dependencies = [
"lazy_static",
"sqlez",
"sqlformat",
"syn 1.0.109",
@@ -11002,6 +10989,7 @@ dependencies = [
"env_logger",
"gpui",
"http_client",
"lazy_static",
"log",
"parking_lot",
"postage",
@@ -12199,6 +12187,7 @@ dependencies = [
"indoc",
"itertools 0.11.0",
"language",
"lazy_static",
"log",
"lsp",
"multi_buffer",
@@ -13541,6 +13530,7 @@ dependencies = [
"http_client",
"itertools 0.11.0",
"language",
"lazy_static",
"log",
"node_runtime",
"parking_lot",
@@ -13824,7 +13814,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.151.0"
version = "0.150.0"
dependencies = [
"activity_indicator",
"anyhow",
@@ -13885,7 +13875,6 @@ dependencies = [
"outline_panel",
"parking_lot",
"paths",
"performance",
"profiling",
"project",
"project_panel",
@@ -13973,7 +13962,7 @@ dependencies = [
[[package]]
name = "zed_elixir"
version = "0.0.8"
version = "0.0.7"
dependencies = [
"zed_extension_api 0.0.6",
]

View File

@@ -70,7 +70,6 @@ members = [
"crates/outline",
"crates/outline_panel",
"crates/paths",
"crates/performance",
"crates/picker",
"crates/prettier",
"crates/project",
@@ -146,7 +145,6 @@ members = [
"extensions/lua",
"extensions/ocaml",
"extensions/php",
"extensions/perplexity",
"extensions/prisma",
"extensions/purescript",
"extensions/ruff",
@@ -243,7 +241,6 @@ open_ai = { path = "crates/open_ai" }
outline = { path = "crates/outline" }
outline_panel = { path = "crates/outline_panel" }
paths = { path = "crates/paths" }
performance = { path = "crates/performance" }
picker = { path = "crates/picker" }
plugin = { path = "crates/plugin" }
plugin_macros = { path = "crates/plugin_macros" }
@@ -359,6 +356,7 @@ isahc = { version = "1.7.2", default-features = false, features = [
] }
itertools = "0.11.0"
jsonwebtoken = "9.3"
lazy_static = "1.4.0"
libc = "0.2"
linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
@@ -382,7 +380,7 @@ rand = "0.8.5"
regex = "1.5"
repair_json = "0.1.0"
rsa = "0.9.6"
runtimelib = { version = "0.15", default-features = false, features = [
runtimelib = { version = "0.14", default-features = false, features = [
"async-dispatcher-runtime",
] }
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }

View File

@@ -1,12 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="16" height="16" rx="2" fill="black" fill-opacity="0.2"/>
<g clip-path="url(#clip0_1916_18)">
<path d="M10.652 3.79999H8.816L12.164 12.2H14L10.652 3.79999Z" fill="#1F1F1E"/>
<path d="M5.348 3.79999L2 12.2H3.872L4.55672 10.436H8.05927L8.744 12.2H10.616L7.268 3.79999H5.348ZM5.16224 8.87599L6.308 5.92399L7.45374 8.87599H5.16224Z" fill="#1F1F1E"/>
</g>
<defs>
<clipPath id="clip0_1916_18">
<rect width="12" height="8.4" fill="white" transform="translate(2 3.79999)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 601 B

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-database-zap"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5V19A9 3 0 0 0 15 21.84"/><path d="M21 5V8"/><path d="M21 12L18 17H22L19 22"/><path d="M3 12A9 3 0 0 0 14.59 14.87"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

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-search-code"><path d="m13 13.5 2-2.5-2-2.5"/><path d="m21 21-4.3-4.3"/><path d="M9 8.5 7 11l2 2.5"/><circle cx="11" cy="11" r="8"/></svg>

Before

Width:  |  Height:  |  Size: 340 B

View File

@@ -1,61 +1,426 @@
{{#if language_name}}
Here's a file of {{language_name}} that I'm going to ask you to make an edit to.
{{else}}
Here's a file of text that I'm going to ask you to make an edit to.
{{/if}}
You are an expert developer assistant working in an AI-enabled text editor.
Your task is to rewrite a specific section of the provided document based on a user-provided prompt.
{{#if is_insert}}
The point you'll need to insert at is marked with <insert_here></insert_here>.
{{else}}
The section you'll need to rewrite is marked with <rewrite_this></rewrite_this> tags.
{{/if}}
<guidelines>
1. Scope: Modify only content within <rewrite_this> tags. Do not alter anything outside these boundaries.
2. Precision: Make changes strictly necessary to fulfill the given prompt. Preserve all other content as-is.
3. Seamless integration: Ensure rewritten sections flow naturally with surrounding text and maintain document structure.
4. Tag exclusion: Never include <rewrite_this>, </rewrite_this>, <edit_here>, or <insert_here> tags in the output.
5. Indentation: Maintain the original indentation level of the file in rewritten sections.
6. Completeness: Rewrite the entire tagged section, even if only partial changes are needed. Avoid omissions or elisions.
7. Insertions: Replace <insert_here></insert_here> tags with appropriate content as specified by the prompt.
8. Code integrity: Respect existing code structure and functionality when making changes.
9. Consistency: Maintain a uniform style and tone throughout the rewritten text.
</guidelines>
<examples>
<example>
<input>
<document>
{{{document_content}}}
use std::cell::Cell;
use std::collections::HashMap;
use std::cmp;
<rewrite_this>
<insert_here></insert_here>
</rewrite_this>
pub struct LruCache<K, V> {
/// The maximum number of items the cache can hold.
capacity: usize,
/// The map storing the cached items.
items: HashMap<K, V>,
}
// The rest of the implementation...
</document>
<prompt>
doc this
</prompt>
</input>
<incorrect_output failure="Over-generation. The text starting with `pub struct AabbTree<T> {` is *after* the rewrite_this tag">
/// Represents an Axis-Aligned Bounding Box (AABB) tree data structure.
///
/// This structure is used for efficient spatial queries and collision detection.
/// It organizes objects in a hierarchical tree structure based on their bounding boxes.
///
/// # Type Parameters
///
/// * `T`: The type of data associated with each node in the tree.
pub struct AabbTree<T> {
root: Option<usize>,
</incorrect_output>
<corrected_output improvement="Generation stops before repeating content after the rewrite_this section">
/// Represents an Axis-Aligned Bounding Box (AABB) tree data structure.
///
/// This structure is used for efficient spatial queries and collision detection.
/// It organizes objects in a hierarchical tree structure based on their bounding boxes.
///
/// # Type Parameters
///
/// * `T`: The type of data associated with each node in the tree.
</corrected_output>
</example>
<example>
<input>
<document>
import math
def calculate_circle_area(radius):
"""Calculate the area of a circle given its radius."""
return math.pi * radius ** 2
<rewrite_this>
<insert_here></insert_here>
</rewrite_this>
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def circumference(self):
return 2 * math.pi * self.radius
# Usage example
circle = Circle(5)
print(f"Area: {circle.area():.2f}")
print(f"Circumference: {circle.circumference():.2f}")
</document>
<prompt>
write docs
</prompt>
</input>
<incorrect_output failure="Over-generation. The text starting with `class Circle:` is *after* the rewrite_this tag">
"""
Represents a circle with methods to calculate its area and circumference.
This class provides a simple way to work with circles in a geometric context.
It allows for the creation of Circle objects with a specified radius and
offers methods to compute the circle's area and circumference.
Attributes:
radius (float): The radius of the circle.
Methods:
area(): Calculates and returns the area of the circle.
circumference(): Calculates and returns the circumference of the circle.
"""
class Circle:
</incorrect_output>
<corrected_output improvement="Generation stops before repeating content after the rewrite_this section">
"""
Represents a circle with methods to calculate its area and circumference.
This class provides a simple way to work with circles in a geometric context.
It allows for the creation of Circle objects with a specified radius and
offers methods to compute the circle's area and circumference.
Attributes:
radius (float): The radius of the circle.
Methods:
area(): Calculates and returns the area of the circle.
circumference(): Calculates and returns the circumference of the circle.
"""
</corrected_output>
</example>
<example>
<input>
<document>
class BankAccount {
private balance: number;
constructor(initialBalance: number) {
this.balance = initialBalance;
}
<rewrite_this>
<insert_here></insert_here>
</rewrite_this>
deposit(amount: number): void {
if (amount > 0) {
this.balance += amount;
}
}
withdraw(amount: number): boolean {
if (amount > 0 && this.balance >= amount) {
this.balance -= amount;
return true;
}
return false;
}
getBalance(): number {
return this.balance;
}
}
// Usage
const account = new BankAccount(1000);
account.deposit(500);
console.log(account.getBalance()); // 1500
account.withdraw(200);
console.log(account.getBalance()); // 1300
</document>
<prompt>
//
</prompt>
</input>
<incorrect_output failure="Over-generation. The text starting with `deposit(amount: number): void {` is *after* the rewrite_this tag">
/**
* Deposits the specified amount into the bank account.
*
* @param amount The amount to deposit. Must be a positive number.
* @throws Error if the amount is not positive.
*/
deposit(amount: number): void {
if (amount > 0) {
this.balance += amount;
} else {
throw new Error("Deposit amount must be positive");
}
}
</incorrect_output>
<corrected_output improvement="Generation stops before repeating content after the rewrite_this section">
/**
* Deposits the specified amount into the bank account.
*
* @param amount The amount to deposit. Must be a positive number.
* @throws Error if the amount is not positive.
*/
</corrected_output>
</example>
<example>
<input>
<document>
use std::collections::VecDeque;
pub struct BinaryTree<T> {
root: Option<Node<T>>,
}
<rewrite_this>
<insert_here></insert_here>
</rewrite_this>
struct Node<T> {
value: T,
left: Option<Box<Node<T>>>,
right: Option<Box<Node<T>>>,
}
</document>
<prompt>
derive clone
</prompt>
</input>
<incorrect_output failure="Over-generation below the rewrite_this tags. Extra space between derive annotation and struct definition.">
#[derive(Clone)]
struct Node<T> {
value: T,
left: Option<Box<Node<T>>>,
right: Option<Box<Node<T>>>,
}
</incorrect_output>
<incorrect_output failure="Over-generation above the rewrite_this tags">
pub struct BinaryTree<T> {
root: Option<Node<T>>,
}
#[derive(Clone)]
</incorrect_output>
<incorrect_output failure="Over-generation below the rewrite_this tags">
#[derive(Clone)]
struct Node<T> {
value: T,
left: Option<Box<Node<T>>>,
right: Option<Box<Node<T>>>,
}
impl<T> Node<T> {
fn new(value: T) -> Self {
Node {
value,
left: None,
right: None,
}
}
}
</incorrect_output>
<corrected_output improvement="Only includes the new content within the rewrite_this tags">
#[derive(Clone)]
</corrected_output>
</example>
<example>
<input>
<document>
import math
def calculate_circle_area(radius):
"""Calculate the area of a circle given its radius."""
return math.pi * radius ** 2
<rewrite_this>
<insert_here></insert_here>
</rewrite_this>
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def circumference(self):
return 2 * math.pi * self.radius
# Usage example
circle = Circle(5)
print(f"Area: {circle.area():.2f}")
print(f"Circumference: {circle.circumference():.2f}")
</document>
<prompt>
add dataclass decorator
</prompt>
</input>
<incorrect_output failure="Over-generation. The text starting with `class Circle:` is *after* the rewrite_this tag">
@dataclass
class Circle:
radius: float
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
</incorrect_output>
<corrected_output improvement="Generation stops before repeating content after the rewrite_this section">
@dataclass
</corrected_output>
</example>
<example>
<input>
<document>
interface ShoppingCart {
items: string[];
total: number;
}
<rewrite_this>
<insert_here></insert_here>class ShoppingCartManager {
</rewrite_this>
private cart: ShoppingCart;
constructor() {
this.cart = { items: [], total: 0 };
}
addItem(item: string, price: number): void {
this.cart.items.push(item);
this.cart.total += price;
}
getTotal(): number {
return this.cart.total;
}
}
// Usage
const manager = new ShoppingCartManager();
manager.addItem("Book", 15.99);
console.log(manager.getTotal()); // 15.99
</document>
<prompt>
add readonly modifier
</prompt>
</input>
<incorrect_output failure="Over-generation. The line starting with ` items: string[];` is *after* the rewrite_this tag">
readonly interface ShoppingCart {
items: string[];
total: number;
}
class ShoppingCartManager {
private readonly cart: ShoppingCart;
constructor() {
this.cart = { items: [], total: 0 };
}
</incorrect_output>
<corrected_output improvement="Only includes the new content within the rewrite_this tags and integrates cleanly into surrounding code">
readonly interface ShoppingCart {
</corrected_output>
</example>
</examples>
With these examples in mind, edit the following file:
<document language="{{ language_name }}">
{{{ document_content }}}
</document>
{{#if is_truncated}}
The context around the relevant section has been truncated (possibly in the middle of a line) for brevity.
The provided document has been truncated (potentially mid-line) for brevity.
{{/if}}
{{#if is_insert}}
You can't replace {{content_type}}, your answer will be inserted in place of the `<insert_here></insert_here>` tags. Don't include the insert_here tags in your output.
Generate {{content_type}} based on the following prompt:
<prompt>
{{{user_prompt}}}
</prompt>
Match the indentation in the original file in the inserted {{content_type}}, don't include any indentation on blank lines.
Immediately start with the following format with no remarks:
```
\{{INSERTED_CODE}}
```
{{else}}
Edit the section of {{content_type}} in <rewrite_this></rewrite_this> tags based on the following prompt:
<prompt>
{{{user_prompt}}}
</prompt>
{{#if rewrite_section}}
And here's the section to rewrite based on that prompt again for reference:
<instructions>
{{#if has_insertion}}
Insert text anywhere you see marked with <insert_here></insert_here> tags. It's CRITICAL that you DO NOT include <insert_here> tags in your output.
{{/if}}
{{#if has_replacement}}
Edit text that you see surrounded with <edit_here>...</edit_here> tags. It's CRITICAL that you DO NOT include <edit_here> tags in your output.
{{/if}}
Make no changes to the rewritten content outside these tags.
<snippet language="{{ language_name }}" annotated="true">
{{{ rewrite_section_prefix }}}
<rewrite_this>
{{{rewrite_section}}}
{{{ rewrite_section_with_edits }}}
</rewrite_this>
{{/if}}
{{{ rewrite_section_suffix }}}
</snippet>
Only make changes that are necessary to fulfill the prompt, leave everything else as-is. All surrounding {{content_type}} will be preserved.
Rewrite the lines enclosed within the <rewrite_this></rewrite_this> tags in accordance with the provided instructions and the prompt below.
Start at the indentation level in the original file in the rewritten {{content_type}}. Don't stop until you've rewritten the entire section, even if you have no more changes to make, always write out the whole section with no unnecessary elisions.
<prompt>
{{{ user_prompt }}}
</prompt>
Do not include <insert_here> or <edit_here> annotations in your output. Here is a clean copy of the snippet without annotations for your reference.
<snippet>
{{{ rewrite_section_prefix }}}
{{{ rewrite_section }}}
{{{ rewrite_section_suffix }}}
</snippet>
</instructions>
<guidelines_reminder>
1. Focus on necessary changes: Modify only what's required to fulfill the prompt.
2. Preserve context: Maintain all surrounding content as-is, ensuring the rewritten section seamlessly integrates with the existing document structure and flow.
3. Exclude annotation tags: Do not output <rewrite_this>, </rewrite_this>, <edit_here>, or <insert_here> tags.
4. Maintain indentation: Begin at the original file's indentation level.
5. Complete rewrite: Continue until the entire section is rewritten, even if no further changes are needed.
6. Avoid elisions: Always write out the full section without unnecessary omissions. NEVER say `// ...` or `// ...existing code` in your output.
7. Respect content boundaries: Preserve code integrity.
</guidelines_reminder>
Immediately start with the following format with no remarks:
```
\{{REWRITTEN_CODE}}
```
{{/if}}

View File

@@ -1,12 +1,11 @@
<workflow>
Guide the user through code changes in numbered steps that focus on individual functions, type definitions, etc.
Guide the user through code changes in numbered steps where each step focuses on a single individual function, type definition, etc.
Surround each distinct step in a <step></step> XML tag. The user will be performing these steps in a code editor
named Zed, which is where they will have entered this prompt and will be seeing the response.
<instructions>
- Use the language of the file for code fence blocks unless otherwise specified.
- Include a code or file action in each step.
- Only put code in separate steps if it should either go in separate files, or in different (non-contiguous) places in the same file.
- Provide error handling and input validation where appropriate.
- Adapt explanations based on the user's perceived level of expertise.
- Include comments in code examples to enhance understanding.

View File

@@ -15,7 +15,6 @@ With each location, you will produce a brief, one-line description of the change
- 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>
@@ -204,7 +203,6 @@ Add a 'use std::fmt;' statement at the beginning of the file
{
"kind": "PrependChild",
"path": "src/vehicle.rs",
"symbol": "#imports",
"description": "Add 'use std::fmt' statement"
}
]
@@ -415,7 +413,6 @@ Add a 'load_from_file' method to Config and import necessary modules
{
"kind": "PrependChild",
"path": "src/config.rs",
"symbol": "#imports",
"description": "Import std::fs and std::io modules"
},
{

View File

@@ -64,8 +64,6 @@
"ui_font_weight": 400,
// The default font size for text in the UI
"ui_font_size": 16,
// How much to fade out unused code.
"unnecessary_code_fade": 0.3,
// The factor to grow the active pane by. Defaults to 1.0
// which gives the same size as all other panes.
"active_pane_magnification": 1.0,
@@ -397,9 +395,9 @@
// The default model to use when creating new contexts.
"default_model": {
// The provider to use.
"provider": "zed.dev",
"provider": "openai",
// The model to use.
"model": "claude-3-5-sonnet"
"model": "gpt-4o"
}
},
// The settings for slash commands.

View File

@@ -33,31 +33,5 @@ services:
volumes:
- ./livekit.yaml:/livekit.yaml
postgrest_app:
image: postgrest/postgrest
container_name: postgrest_app
ports:
- 8081:8081
environment:
PGRST_DB_URI: postgres://postgres@postgres:5432/zed
volumes:
- ./crates/collab/postgrest_app.conf:/etc/postgrest.conf
command: postgrest /etc/postgrest.conf
depends_on:
- postgres
postgrest_llm:
image: postgrest/postgrest
container_name: postgrest_llm
ports:
- 8082:8082
environment:
PGRST_DB_URI: postgres://postgres@postgres:5432/zed_llm
volumes:
- ./crates/collab/postgrest_llm.conf:/etc/postgrest.conf
command: postgrest /etc/postgrest.conf
depends_on:
- postgres
volumes:
postgres_data:

View File

@@ -17,7 +17,6 @@ path = "src/anthropic.rs"
[dependencies]
anyhow.workspace = true
chrono.workspace = true
futures.workspace = true
http_client.workspace = true
isahc.workspace = true
@@ -26,7 +25,6 @@ serde.workspace = true
serde_json.workspace = true
strum.workspace = true
thiserror.workspace = true
util.workspace = true
[dev-dependencies]
tokio.workspace = true

View File

@@ -1,17 +1,14 @@
mod supported_countries;
use anyhow::{anyhow, Context, Result};
use chrono::{DateTime, Utc};
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use isahc::config::Configurable;
use isahc::http::{HeaderMap, HeaderValue};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use std::{pin::Pin, str::FromStr};
use strum::{EnumIter, EnumString};
use thiserror::Error;
use util::ResultExt as _;
pub use supported_countries::*;
@@ -41,8 +38,6 @@ pub enum Model {
Custom {
name: String,
max_tokens: usize,
/// The name displayed in the UI, such as in the assistant panel model dropdown menu.
display_name: Option<String>,
/// Override this model with a different Anthropic model for tool calls.
tool_override: Option<String>,
/// Indicates whether this custom model supports caching.
@@ -82,9 +77,7 @@ impl Model {
Self::Claude3Opus => "Claude 3 Opus",
Self::Claude3Sonnet => "Claude 3 Sonnet",
Self::Claude3Haiku => "Claude 3 Haiku",
Self::Custom {
name, display_name, ..
} => display_name.as_ref().unwrap_or(name),
Self::Custom { name, .. } => name,
}
}
@@ -198,66 +191,6 @@ pub async fn stream_completion(
request: Request,
low_speed_timeout: Option<Duration>,
) -> Result<BoxStream<'static, Result<Event, AnthropicError>>, AnthropicError> {
stream_completion_with_rate_limit_info(client, api_url, api_key, request, low_speed_timeout)
.await
.map(|output| output.0)
}
/// https://docs.anthropic.com/en/api/rate-limits#response-headers
#[derive(Debug)]
pub struct RateLimitInfo {
pub requests_limit: usize,
pub requests_remaining: usize,
pub requests_reset: DateTime<Utc>,
pub tokens_limit: usize,
pub tokens_remaining: usize,
pub tokens_reset: DateTime<Utc>,
}
impl RateLimitInfo {
fn from_headers(headers: &HeaderMap<HeaderValue>) -> Result<Self> {
let tokens_limit = get_header("anthropic-ratelimit-tokens-limit", headers)?.parse()?;
let requests_limit = get_header("anthropic-ratelimit-requests-limit", headers)?.parse()?;
let tokens_remaining =
get_header("anthropic-ratelimit-tokens-remaining", headers)?.parse()?;
let requests_remaining =
get_header("anthropic-ratelimit-requests-remaining", headers)?.parse()?;
let requests_reset = get_header("anthropic-ratelimit-requests-reset", headers)?;
let tokens_reset = get_header("anthropic-ratelimit-tokens-reset", headers)?;
let requests_reset = DateTime::parse_from_rfc3339(requests_reset)?.to_utc();
let tokens_reset = DateTime::parse_from_rfc3339(tokens_reset)?.to_utc();
Ok(Self {
requests_limit,
tokens_limit,
requests_remaining,
tokens_remaining,
requests_reset,
tokens_reset,
})
}
}
fn get_header<'a>(key: &str, headers: &'a HeaderMap) -> Result<&'a str, anyhow::Error> {
Ok(headers
.get(key)
.ok_or_else(|| anyhow!("missing header `{key}`"))?
.to_str()?)
}
pub async fn stream_completion_with_rate_limit_info(
client: &dyn HttpClient,
api_url: &str,
api_key: &str,
request: Request,
low_speed_timeout: Option<Duration>,
) -> Result<
(
BoxStream<'static, Result<Event, AnthropicError>>,
Option<RateLimitInfo>,
),
AnthropicError,
> {
let request = StreamingRequest {
base: request,
stream: true,
@@ -287,9 +220,8 @@ pub async fn stream_completion_with_rate_limit_info(
.await
.context("failed to send request to Anthropic")?;
if response.status().is_success() {
let rate_limits = RateLimitInfo::from_headers(response.headers());
let reader = BufReader::new(response.into_body());
let stream = reader
Ok(reader
.lines()
.filter_map(|line| async move {
match line {
@@ -303,8 +235,7 @@ pub async fn stream_completion_with_rate_limit_info(
Err(error) => Some(Err(AnthropicError::Other(anyhow!(error)))),
}
})
.boxed();
Ok((stream, rate_limits.log_err()))
.boxed())
} else {
let mut body = Vec::new();
response

View File

@@ -9,7 +9,7 @@ mod model_selector;
mod prompt_library;
mod prompts;
mod slash_command;
pub(crate) mod slash_command_picker;
mod slash_command_picker;
pub mod slash_command_settings;
mod streaming_diff;
mod terminal_inline_assistant;
@@ -34,7 +34,7 @@ use language_model::{
};
pub(crate) use model_selector::*;
pub use prompts::PromptBuilder;
use prompts::PromptLoadingParams;
use prompts::PromptOverrideContext;
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
use serde::{Deserialize, Serialize};
use settings::{update_settings_file, Settings, SettingsStore};
@@ -184,7 +184,7 @@ impl Assistant {
pub fn init(
fs: Arc<dyn Fs>,
client: Arc<Client>,
stdout_is_a_pty: bool,
dev_mode: bool,
cx: &mut AppContext,
) -> Arc<PromptBuilder> {
cx.set_global(Assistant::default());
@@ -223,11 +223,9 @@ pub fn init(
assistant_panel::init(cx);
context_servers::init(cx);
let prompt_builder = prompts::PromptBuilder::new(Some(PromptLoadingParams {
let prompt_builder = prompts::PromptBuilder::new(Some(PromptOverrideContext {
dev_mode,
fs: fs.clone(),
repo_path: stdout_is_a_pty
.then(|| std::env::current_dir().log_err())
.flatten(),
cx,
}))
.log_err()

View File

@@ -11,11 +11,11 @@ use crate::{
},
slash_command_picker,
terminal_inline_assistant::TerminalInlineAssistant,
Assist, CacheStatus, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore,
CycleMessageRole, DeployHistory, DeployPromptLibrary, InlineAssist, InlineAssistId,
InlineAssistant, InsertIntoEditor, MessageStatus, ModelSelector, PendingSlashCommand,
PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata, SavedContextMetadata, Split,
ToggleFocus, ToggleModelSelector, WorkflowStepResolution, WorkflowStepView,
Assist, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore, CycleMessageRole,
DeployHistory, DeployPromptLibrary, InlineAssist, InlineAssistId, InlineAssistant,
InsertIntoEditor, MessageStatus, ModelSelector, PendingSlashCommand, PendingSlashCommandStatus,
QuoteSelection, RemoteContextMetadata, SavedContextMetadata, Split, ToggleFocus,
ToggleModelSelector, WorkflowStepResolution, WorkflowStepView,
};
use crate::{ContextStoreEvent, ModelPickerDelegate};
use anyhow::{anyhow, Result};
@@ -36,10 +36,10 @@ use fs::Fs;
use gpui::{
canvas, div, img, percentage, point, pulsating_between, size, Action, Animation, AnimationExt,
AnyElement, AnyView, AppContext, AsyncWindowContext, ClipboardEntry, ClipboardItem,
Context as _, DismissEvent, Empty, Entity, EntityId, EventEmitter, FocusHandle, FocusableView,
FontWeight, InteractiveElement, IntoElement, Model, ParentElement, Pixels, ReadGlobal, Render,
RenderImage, SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task,
Transformation, UpdateGlobal, View, VisualContext, WeakView, WindowContext,
Context as _, CursorStyle, DismissEvent, Empty, Entity, EntityId, EventEmitter, FocusHandle,
FocusableView, FontWeight, InteractiveElement, IntoElement, Model, ParentElement, Pixels,
ReadGlobal, Render, RenderImage, SharedString, Size, StatefulInteractiveElement, Styled,
Subscription, Task, Transformation, UpdateGlobal, View, VisualContext, WeakView, WindowContext,
};
use indexed_docs::IndexedDocsStore;
use language::{
@@ -69,8 +69,8 @@ use ui::TintColor;
use ui::{
prelude::*,
utils::{format_distance_from_now, DateTimeType},
Avatar, AvatarShape, ButtonLike, ContextMenu, Disclosure, ElevationIndex, IconButtonShape,
KeyBinding, ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tooltip,
Avatar, AvatarShape, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tooltip,
};
use util::ResultExt;
use workspace::{
@@ -543,18 +543,19 @@ impl AssistantPanel {
cx.emit(AssistantPanelEvent::ContextEdited);
true
}
pane::Event::RemovedItem { .. } => {
let has_configuration_view = self
pane::Event::RemoveItem { idx } => {
if self
.pane
.read(cx)
.items_of_type::<ConfigurationView>()
.next()
.is_some();
if !has_configuration_view {
.item_for_index(*idx)
.map_or(false, |item| item.downcast::<ConfigurationView>().is_some())
{
self.configuration_subscription = None;
}
false
}
pane::Event::RemovedItem { .. } => {
cx.emit(AssistantPanelEvent::ContextEdited);
true
}
@@ -1355,7 +1356,6 @@ struct WorkflowStep {
footer_block_id: CustomBlockId,
resolved_step: Option<Result<WorkflowStepResolution, Arc<anyhow::Error>>>,
assist: Option<WorkflowAssist>,
auto_apply: bool,
}
impl WorkflowStep {
@@ -1392,16 +1392,13 @@ impl WorkflowStep {
}
}
Some(Err(error)) => WorkflowStepStatus::Error(error.clone()),
None => WorkflowStepStatus::Resolving {
auto_apply: self.auto_apply,
},
None => WorkflowStepStatus::Resolving,
}
}
}
#[derive(Clone)]
enum WorkflowStepStatus {
Resolving { auto_apply: bool },
Resolving,
Error(Arc<anyhow::Error>),
Empty,
Idle,
@@ -1478,6 +1475,16 @@ impl WorkflowStepStatus {
.unwrap_or_default()
}
match self {
WorkflowStepStatus::Resolving => Label::new("Resolving")
.size(LabelSize::Small)
.with_animation(
("resolving-suggestion-animation", id),
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 0.8)),
|label, delta| label.alpha(delta),
)
.into_any_element(),
WorkflowStepStatus::Error(error) => Self::render_workflow_step_error(
id,
editor.clone(),
@@ -1490,72 +1497,43 @@ impl WorkflowStepStatus {
step_range.clone(),
"Model was unable to locate the code to edit".to_string(),
),
WorkflowStepStatus::Idle | WorkflowStepStatus::Resolving { .. } => {
let status = self.clone();
Button::new(("transform", id), "Transform")
.icon(IconName::SparkleAlt)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.label_size(LabelSize::Small)
.style(ButtonStyle::Tinted(TintColor::Accent))
.tooltip({
let step_range = step_range.clone();
let editor = editor.clone();
move |cx| {
cx.new_view(|cx| {
let tooltip = Tooltip::new("Transform");
if display_keybind_in_tooltip(&step_range, &editor, cx) {
tooltip.key_binding(KeyBinding::for_action_in(
&Assist,
&focus_handle,
cx,
))
} else {
tooltip
}
})
.into()
}
})
.on_click({
let editor = editor.clone();
let step_range = step_range.clone();
move |_, cx| {
if let WorkflowStepStatus::Idle = &status {
editor
.update(cx, |this, cx| {
this.apply_workflow_step(step_range.clone(), cx)
})
.ok();
} else if let WorkflowStepStatus::Resolving { auto_apply: false } =
&status
{
editor
.update(cx, |this, _| {
if let Some(step) = this.workflow_steps.get_mut(&step_range)
{
step.auto_apply = true;
}
})
.ok();
WorkflowStepStatus::Idle => Button::new(("transform", id), "Transform")
.icon(IconName::SparkleAlt)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.label_size(LabelSize::Small)
.style(ButtonStyle::Tinted(TintColor::Accent))
.tooltip({
let step_range = step_range.clone();
let editor = editor.clone();
move |cx| {
cx.new_view(|cx| {
let tooltip = Tooltip::new("Transform");
if display_keybind_in_tooltip(&step_range, &editor, cx) {
tooltip.key_binding(KeyBinding::for_action_in(
&Assist,
&focus_handle,
cx,
))
} else {
tooltip
}
}
})
.map(|this| {
if let WorkflowStepStatus::Resolving { auto_apply: true } = &self {
this.with_animation(
("resolving-suggestion-animation", id),
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 0.8)),
|label, delta| label.alpha(delta),
)
.into_any_element()
} else {
this.into_any_element()
}
})
}
})
.into()
}
})
.on_click({
let editor = editor.clone();
let step_range = step_range.clone();
move |_, cx| {
editor
.update(cx, |this, cx| {
this.apply_workflow_step(step_range.clone(), cx)
})
.ok();
}
})
.into_any_element(),
WorkflowStepStatus::Pending => h_flex()
.items_center()
.gap_2()
@@ -1719,6 +1697,7 @@ struct WorkflowAssist {
editor: WeakView<Editor>,
editor_was_open: bool,
assist_ids: Vec<InlineAssistId>,
_observe_assist_status: Task<()>,
}
pub struct ContextEditor {
@@ -1740,8 +1719,7 @@ pub struct ContextEditor {
assistant_panel: WeakView<AssistantPanel>,
error_message: Option<SharedString>,
show_accept_terms: bool,
pub(crate) slash_menu_handle:
PopoverMenuHandle<Picker<slash_command_picker::SlashCommandDelegate>>,
slash_menu_handle: PopoverMenuHandle<ContextMenu>,
}
const DEFAULT_TAB_TITLE: &str = "New Context";
@@ -1807,25 +1785,30 @@ impl ContextEditor {
};
this.update_message_headers(cx);
this.update_image_blocks(cx);
this.insert_slash_command_output_sections(sections, false, cx);
this.insert_slash_command_output_sections(sections, cx);
this
}
fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) {
let command_name = DefaultSlashCommand.name();
self.editor.update(cx, |editor, cx| {
editor.insert(&format!("/{command_name}\n\n"), cx)
editor.insert(&format!("/{command_name}"), cx)
});
self.split(&Split, cx);
let command = self.context.update(cx, |context, cx| {
let first_message_id = context.messages(cx).next().unwrap().id;
context.update_metadata(first_message_id, cx, |metadata| {
metadata.role = Role::System;
});
context.reparse_slash_commands(cx);
context.pending_slash_commands()[0].clone()
});
self.run_command(
command.source_range,
&command.name,
&command.arguments,
false,
false,
self.workspace.clone(),
cx,
);
@@ -1855,25 +1838,13 @@ impl ContextEditor {
if let Some(workflow_step) = self.workflow_steps.get(&range) {
if let Some(assist) = workflow_step.assist.as_ref() {
let assist_ids = assist.assist_ids.clone();
cx.spawn(|this, mut cx| async move {
for assist_id in assist_ids {
let mut receiver = this.update(&mut cx, |_, cx| {
cx.window_context().defer(move |cx| {
InlineAssistant::update_global(cx, |assistant, cx| {
assistant.start_assist(assist_id, cx);
})
});
InlineAssistant::update_global(cx, |assistant, _| {
assistant.observe_assist(assist_id)
})
})?;
while !receiver.borrow().is_done() {
let _ = receiver.changed().await;
cx.window_context().defer(|cx| {
InlineAssistant::update_global(cx, |assistant, cx| {
for assist_id in assist_ids {
assistant.start_assist(assist_id, cx);
}
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
})
});
}
}
}
@@ -1885,7 +1856,7 @@ impl ContextEditor {
let range = step.range.clone();
match step.status(cx) {
WorkflowStepStatus::Resolving { .. } | WorkflowStepStatus::Pending => true,
WorkflowStepStatus::Resolving | WorkflowStepStatus::Pending => true,
WorkflowStepStatus::Idle => {
self.apply_workflow_step(range, cx);
true
@@ -2104,7 +2075,6 @@ impl ContextEditor {
&command.name,
&command.arguments,
true,
false,
workspace.clone(),
cx,
);
@@ -2113,27 +2083,19 @@ impl ContextEditor {
}
}
#[allow(clippy::too_many_arguments)]
pub fn run_command(
&mut self,
command_range: Range<language::Anchor>,
name: &str,
arguments: &[String],
ensure_trailing_newline: bool,
expand_result: bool,
insert_trailing_newline: bool,
workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>,
) {
if let Some(command) = SlashCommandRegistry::global(cx).command(name) {
let output = command.run(arguments, workspace, self.lsp_adapter_delegate.clone(), cx);
self.context.update(cx, |context, cx| {
context.insert_command_output(
command_range,
output,
ensure_trailing_newline,
expand_result,
cx,
)
context.insert_command_output(command_range, output, insert_trailing_newline, cx)
});
}
}
@@ -2219,7 +2181,6 @@ impl ContextEditor {
&command.name,
&command.arguments,
false,
false,
workspace.clone(),
cx,
);
@@ -2316,13 +2277,8 @@ impl ContextEditor {
output_range,
sections,
run_commands_in_output,
expand_result,
} => {
self.insert_slash_command_output_sections(
sections.iter().cloned(),
*expand_result,
cx,
);
self.insert_slash_command_output_sections(sections.iter().cloned(), cx);
if *run_commands_in_output {
let commands = self.context.update(cx, |context, cx| {
@@ -2338,7 +2294,6 @@ impl ContextEditor {
&command.name,
&command.arguments,
false,
false,
self.workspace.clone(),
cx,
);
@@ -2355,7 +2310,6 @@ impl ContextEditor {
fn insert_slash_command_output_sections(
&mut self,
sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
expand_result: bool,
cx: &mut ViewContext<Self>,
) {
self.editor.update(cx, |editor, cx| {
@@ -2410,9 +2364,6 @@ impl ContextEditor {
editor.insert_creases(creases, cx);
if expand_result {
buffer_rows_to_fold.clear();
}
for buffer_row in buffer_rows_to_fold.into_iter().rev() {
editor.fold_at(&FoldAt { buffer_row }, cx);
}
@@ -2484,18 +2435,6 @@ impl ContextEditor {
};
let resolved_step = step.read(cx).resolution.clone();
if let Some(Ok(resolution)) = resolved_step.as_ref() {
for (buffer, _) in resolution.suggestion_groups.iter() {
let step_range = step_range.clone();
cx.subscribe(buffer, move |this, _, event, cx| match event {
language::Event::Discarded => this.undo_workflow_step(step_range.clone(), cx),
_ => {}
})
.detach();
}
}
if let Some(existing_step) = self.workflow_steps.get_mut(&step_range) {
existing_step.resolved_step = resolved_step;
} else {
@@ -2600,35 +2539,19 @@ impl ContextEditor {
div().child(step_label)
};
let step_label_element = step_label.into_any_element();
let step_label = h_flex()
let step_label = step_label
.id("step")
.group("step-label")
.items_center()
.gap_1()
.child(step_label_element)
.child(
IconButton::new("edit-step", IconName::SearchCode)
.size(ButtonSize::Compact)
.icon_size(IconSize::Small)
.shape(IconButtonShape::Square)
.visible_on_hover("step-label")
.tooltip(|cx| Tooltip::text("Open Step View", cx))
.on_click({
let this = weak_self.clone();
let step_range = step_range.clone();
move |_, cx| {
this.update(cx, |this, cx| {
this.open_workflow_step(
step_range.clone(),
cx,
);
})
.ok();
}
}),
);
.cursor(CursorStyle::PointingHand)
.on_click({
let this = weak_self.clone();
let step_range = step_range.clone();
move |_, cx| {
this.update(cx, |this, cx| {
this.open_workflow_step(step_range.clone(), cx);
})
.ok();
}
});
div()
.w_full()
@@ -2711,17 +2634,11 @@ impl ContextEditor {
footer_block_id: block_ids[1],
resolved_step,
assist: None,
auto_apply: false,
},
);
}
self.update_active_workflow_step(cx);
if let Some(step) = self.workflow_steps.get_mut(&step_range) {
if step.auto_apply && matches!(step.status(cx), WorkflowStepStatus::Idle) {
self.apply_workflow_step(step_range, cx);
}
}
}
fn open_workflow_step(
@@ -2759,25 +2676,14 @@ impl ContextEditor {
fn update_active_workflow_step(&mut self, cx: &mut ViewContext<Self>) {
let new_step = self.active_workflow_step_for_cursor(cx);
if new_step.as_ref() != self.active_workflow_step.as_ref() {
let mut old_editor = None;
let mut old_editor_was_open = None;
if let Some(old_step) = self.active_workflow_step.take() {
(old_editor, old_editor_was_open) =
self.hide_workflow_step(old_step.range, cx).unzip();
self.hide_workflow_step(old_step.range, cx);
}
let mut new_editor = None;
if let Some(new_step) = new_step {
new_editor = self.show_workflow_step(new_step.range.clone(), cx);
self.show_workflow_step(new_step.range.clone(), cx);
self.active_workflow_step = Some(new_step);
}
if new_editor != old_editor {
if let Some((old_editor, old_editor_was_open)) = old_editor.zip(old_editor_was_open)
{
self.close_workflow_editor(cx, old_editor, old_editor_was_open)
}
}
}
}
@@ -2785,15 +2691,15 @@ impl ContextEditor {
&mut self,
step_range: Range<language::Anchor>,
cx: &mut ViewContext<Self>,
) -> Option<(View<Editor>, bool)> {
) {
let Some(step) = self.workflow_steps.get_mut(&step_range) else {
return None;
return;
};
let Some(assist) = step.assist.as_ref() else {
return None;
return;
};
let Some(editor) = assist.editor.upgrade() else {
return None;
return;
};
if matches!(step.status(cx), WorkflowStepStatus::Idle) {
@@ -2803,42 +2709,32 @@ impl ContextEditor {
assistant.finish_assist(assist_id, true, cx)
}
});
return Some((editor, assist.editor_was_open));
self.workspace
.update(cx, |workspace, cx| {
if let Some(pane) = workspace.pane_for(&editor) {
pane.update(cx, |pane, cx| {
let item_id = editor.entity_id();
if !assist.editor_was_open && pane.is_active_preview_item(item_id) {
pane.close_item_by_id(item_id, SaveIntent::Skip, cx)
.detach_and_log_err(cx);
}
});
}
})
.ok();
}
return None;
}
fn close_workflow_editor(
&mut self,
cx: &mut ViewContext<ContextEditor>,
editor: View<Editor>,
editor_was_open: bool,
) {
self.workspace
.update(cx, |workspace, cx| {
if let Some(pane) = workspace.pane_for(&editor) {
pane.update(cx, |pane, cx| {
let item_id = editor.entity_id();
if !editor_was_open && pane.is_active_preview_item(item_id) {
pane.close_item_by_id(item_id, SaveIntent::Skip, cx)
.detach_and_log_err(cx);
}
});
}
})
.ok();
}
fn show_workflow_step(
&mut self,
step_range: Range<language::Anchor>,
cx: &mut ViewContext<Self>,
) -> Option<View<Editor>> {
) {
let Some(step) = self.workflow_steps.get_mut(&step_range) else {
return None;
return;
};
let mut editor_to_return = None;
let mut scroll_to_assist_id = None;
match step.status(cx) {
WorkflowStepStatus::Idle => {
@@ -2852,10 +2748,6 @@ impl ContextEditor {
&self.workspace,
cx,
);
editor_to_return = step
.assist
.as_ref()
.and_then(|assist| assist.editor.upgrade());
}
}
WorkflowStepStatus::Pending => {
@@ -2877,15 +2769,14 @@ impl ContextEditor {
}
if let Some(assist_id) = scroll_to_assist_id {
if let Some(assist_editor) = step
if let Some(editor) = step
.assist
.as_ref()
.and_then(|assists| assists.editor.upgrade())
{
editor_to_return = Some(assist_editor.clone());
self.workspace
.update(cx, |workspace, cx| {
workspace.activate_item(&assist_editor, false, false, cx);
workspace.activate_item(&editor, false, false, cx);
})
.ok();
InlineAssistant::update_global(cx, |assistant, cx| {
@@ -2893,8 +2784,6 @@ impl ContextEditor {
});
}
}
return editor_to_return;
}
fn open_assists_for_step(
@@ -2938,6 +2827,7 @@ impl ContextEditor {
)
})
.log_err()?;
let (&excerpt_id, _, _) = editor
.read(cx)
.buffer()
@@ -3011,10 +2901,35 @@ impl ContextEditor {
}
}
let mut observations = Vec::new();
InlineAssistant::update_global(cx, |assistant, _cx| {
for assist_id in &assist_ids {
observations.push(assistant.observe_assist(*assist_id));
}
});
Some(WorkflowAssist {
assist_ids,
editor: editor.downgrade(),
editor_was_open,
_observe_assist_status: cx.spawn(|this, mut cx| async move {
while !observations.is_empty() {
let (result, ix, _) = futures::future::select_all(
observations
.iter_mut()
.map(|observation| Box::pin(observation.changed())),
)
.await;
if result.is_err() {
observations.remove(ix);
}
if this.update(&mut cx, |_, cx| cx.notify()).is_err() {
break;
}
}
}),
})
}
@@ -3131,36 +3046,6 @@ impl ContextEditor {
.relative()
.gap_1()
.child(sender)
.children(match &message.cache {
Some(cache) if cache.is_final_anchor => match cache.status {
CacheStatus::Cached => Some(
div()
.id("cached")
.child(
Icon::new(IconName::DatabaseZap)
.size(IconSize::XSmall)
.color(Color::Hint),
)
.tooltip(|cx| {
Tooltip::with_meta(
"Context cached",
None,
"Large messages cached to optimize performance",
cx,
)
}).into_any_element()
),
CacheStatus::Pending => Some(
div()
.child(
Icon::new(IconName::Ellipsis)
.size(IconSize::XSmall)
.color(Color::Hint),
).into_any_element()
),
},
_ => None,
})
.children(match &message.status {
MessageStatus::Error(error) => Some(
Button::new("show-error", "Error")
@@ -3621,8 +3506,7 @@ impl ContextEditor {
};
Some(
h_flex()
.px_3()
.py_2()
.p_3()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().editor_background)
@@ -3658,17 +3542,13 @@ impl ContextEditor {
fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx).clone();
let mut should_pulsate = false;
let button_text = match self.active_workflow_step() {
Some(step) => match step.status(cx) {
WorkflowStepStatus::Resolving => "Resolving Step...",
WorkflowStepStatus::Empty | WorkflowStepStatus::Error(_) => "Retry Step Resolution",
WorkflowStepStatus::Resolving { auto_apply } => {
should_pulsate = auto_apply;
"Transform"
}
WorkflowStepStatus::Idle => "Transform",
WorkflowStepStatus::Pending => "Applying...",
WorkflowStepStatus::Done => "Accept",
WorkflowStepStatus::Pending => "Transforming...",
WorkflowStepStatus::Done => "Accept Transformation",
WorkflowStepStatus::Confirmed => "Send",
},
None => "Send",
@@ -3698,12 +3578,12 @@ impl ContextEditor {
let provider = LanguageModelRegistry::read_global(cx).active_provider();
let has_configuration_error = configuration_error(cx).is_some();
let needs_to_accept_terms = self.show_accept_terms
&& provider
.as_ref()
.map_or(false, |provider| provider.must_accept_terms(cx));
let disabled = has_configuration_error || needs_to_accept_terms;
let has_active_error = self.error_message.is_some();
let disabled = needs_to_accept_terms || has_active_error;
ButtonLike::new("send_button")
.disabled(disabled)
@@ -3712,20 +3592,7 @@ impl ContextEditor {
button.tooltip(move |_| tooltip.clone())
})
.layer(ElevationIndex::ModalSurface)
.child(Label::new(button_text).map(|this| {
if should_pulsate {
this.with_animation(
"resolving-suggestion-send-button-animation",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 0.8)),
|label, delta| label.alpha(delta),
)
.into_any_element()
} else {
this.into_any_element()
}
}))
.child(Label::new(button_text))
.children(
KeyBinding::for_action_in(&Assist, &focus_handle, cx)
.map(|binding| binding.into_any_element()),
@@ -3788,8 +3655,8 @@ impl Render for ContextEditor {
this.child(
div()
.absolute()
.right_3()
.bottom_12()
.right_4()
.bottom_10()
.max_w_96()
.py_2()
.px_3()
@@ -3803,8 +3670,8 @@ impl Render for ContextEditor {
this.child(
div()
.absolute()
.right_3()
.bottom_12()
.right_4()
.bottom_10()
.max_w_96()
.py_2()
.px_3()
@@ -3815,12 +3682,12 @@ impl Render for ContextEditor {
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.gap_1()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(
Label::new("Error interacting with language model")
.weight(FontWeight::MEDIUM),
.weight(FontWeight::SEMIBOLD),
),
)
.child(
@@ -3861,15 +3728,17 @@ impl Render for ContextEditor {
})
.tooltip(move |cx| {
cx.new_view(|cx| {
Tooltip::new("Insert Selection").key_binding(
focus_handle.as_ref().and_then(|handle| {
KeyBinding::for_action_in(
&QuoteSelection,
&handle,
cx,
)
}),
)
Tooltip::new("Insert Selection")
.meta("Press to quote via keyboard")
.key_binding(focus_handle.as_ref().and_then(
|handle| {
KeyBinding::for_action_in(
&QuoteSelection,
&handle,
cx,
)
},
))
})
.into()
}),
@@ -4251,7 +4120,7 @@ impl Render for ContextEditorToolbarItem {
(Some(provider), Some(model)) => h_flex()
.gap_1()
.child(
Icon::new(model.icon().unwrap_or_else(|| provider.icon()))
Icon::new(provider.icon())
.color(Color::Muted)
.size(IconSize::XSmall),
)
@@ -4526,7 +4395,6 @@ impl ConfigurationView {
provider: &Arc<dyn LanguageModelProvider>,
cx: &mut ViewContext<Self>,
) -> Div {
let provider_id = provider.id().0.clone();
let provider_name = provider.name().0.clone();
let configuration_view = self.configuration_views.get(&provider.id()).cloned();
@@ -4548,15 +4416,12 @@ impl ConfigurationView {
.when(provider.is_authenticated(cx), move |this| {
this.child(
h_flex().justify_end().child(
Button::new(
SharedString::from(format!("new-context-{provider_id}")),
"Open new context",
)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.on_click(open_new_context),
Button::new("new-context", "Open new context")
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.on_click(open_new_context),
),
)
}),

View File

@@ -153,8 +153,8 @@ impl AssistantSettingsContent {
models
.into_iter()
.filter_map(|model| match model {
open_ai::Model::Custom { name, max_tokens,max_output_tokens } => {
Some(language_model::provider::open_ai::AvailableModel { name, max_tokens,max_output_tokens })
open_ai::Model::Custom { name, max_tokens } => {
Some(language_model::provider::open_ai::AvailableModel { name, max_tokens })
}
_ => None,
})
@@ -543,8 +543,8 @@ mod tests {
assert_eq!(
AssistantSettings::get_global(cx).default_model,
LanguageModelSelection {
provider: "zed.dev".into(),
model: "claude-3-5-sonnet".into(),
provider: "openai".into(),
model: "gpt-4o".into(),
}
);
});

View File

@@ -40,7 +40,6 @@ use std::{
time::{Duration, Instant},
};
use telemetry_events::AssistantKind;
use text::BufferSnapshot;
use util::{post_inc, ResultExt, TryFutureExt};
use uuid::Uuid;
@@ -108,7 +107,8 @@ impl ContextOperation {
message.status.context("invalid status")?,
),
timestamp: id.0,
cache: None,
should_cache: false,
is_cache_anchor: false,
},
version: language::proto::deserialize_version(&insert.version),
})
@@ -123,7 +123,8 @@ impl ContextOperation {
timestamp: language::proto::deserialize_timestamp(
update.timestamp.context("invalid timestamp")?,
),
cache: None,
should_cache: false,
is_cache_anchor: false,
},
version: language::proto::deserialize_version(&update.version),
}),
@@ -294,7 +295,6 @@ pub enum ContextEvent {
output_range: Range<language::Anchor>,
sections: Vec<SlashCommandOutputSection<language::Anchor>>,
run_commands_in_output: bool,
expand_result: bool,
},
Operation(ContextOperation),
}
@@ -312,43 +312,13 @@ pub struct MessageAnchor {
pub start: language::Anchor,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CacheStatus {
Pending,
Cached,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MessageCacheMetadata {
pub is_anchor: bool,
pub is_final_anchor: bool,
pub status: CacheStatus,
pub cached_at: clock::Global,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct MessageMetadata {
pub role: Role,
pub status: MessageStatus,
timestamp: clock::Lamport,
#[serde(skip)]
pub cache: Option<MessageCacheMetadata>,
}
impl MessageMetadata {
pub fn is_cache_valid(&self, buffer: &BufferSnapshot, range: &Range<usize>) -> bool {
let result = match &self.cache {
Some(MessageCacheMetadata { cached_at, .. }) => !buffer.has_edits_since_in_range(
&cached_at,
Range {
start: buffer.anchor_at(range.start, Bias::Right),
end: buffer.anchor_at(range.end, Bias::Left),
},
),
_ => false,
};
result
}
should_cache: bool,
is_cache_anchor: bool,
}
#[derive(Clone, Debug)]
@@ -374,7 +344,7 @@ pub struct Message {
pub anchor: language::Anchor,
pub role: Role,
pub status: MessageStatus,
pub cache: Option<MessageCacheMetadata>,
pub cache: bool,
}
impl Message {
@@ -410,7 +380,7 @@ impl Message {
Some(LanguageModelRequestMessage {
role: self.role,
content,
cache: self.cache.as_ref().map_or(false, |cache| cache.is_anchor),
cache: self.cache,
})
}
@@ -573,7 +543,8 @@ impl Context {
role: Role::User,
status: MessageStatus::Done,
timestamp: first_message_id.0,
cache: None,
should_cache: false,
is_cache_anchor: false,
},
);
this.message_anchors.push(message);
@@ -803,7 +774,6 @@ impl Context {
cx.emit(ContextEvent::SlashCommandFinished {
output_range,
sections,
expand_result: false,
run_commands_in_output: false,
});
}
@@ -1007,7 +977,7 @@ impl Context {
});
}
pub fn mark_cache_anchors(
pub fn mark_longest_messages_for_cache(
&mut self,
cache_configuration: &Option<LanguageModelCacheConfiguration>,
speculative: bool,
@@ -1022,104 +992,66 @@ impl Context {
min_total_token: 0,
});
let messages: Vec<Message> = self.messages(cx).collect();
let messages: Vec<Message> = self
.messages_from_anchors(
self.message_anchors.iter().take(if speculative {
self.message_anchors.len().saturating_sub(1)
} else {
self.message_anchors.len()
}),
cx,
)
.filter(|message| message.offset_range.len() >= 5_000)
.collect();
let mut sorted_messages = messages.clone();
if speculative {
// Avoid caching the last message if this is a speculative cache fetch as
// it's likely to change.
sorted_messages.pop();
}
sorted_messages.retain(|m| m.role == Role::User);
sorted_messages.sort_by(|a, b| b.offset_range.len().cmp(&a.offset_range.len()));
let cache_anchors = if self.token_count.unwrap_or(0) < cache_configuration.min_total_token {
// If we have't hit the minimum threshold to enable caching, don't cache anything.
0
if cache_configuration.max_cache_anchors == 0 && cache_configuration.should_speculate {
// Some models support caching, but don't support anchors. In that case we want to
// mark the largest message as needing to be cached, but we will not mark it as an
// anchor.
sorted_messages.truncate(1);
} else {
// Save 1 anchor for the inline assistant to use.
max(cache_configuration.max_cache_anchors, 1) - 1
};
sorted_messages.truncate(cache_anchors);
// Save 1 anchor for the inline assistant.
sorted_messages.truncate(max(cache_configuration.max_cache_anchors, 1) - 1);
}
let anchors: HashSet<MessageId> = sorted_messages
let longest_message_ids: HashSet<MessageId> = sorted_messages
.into_iter()
.map(|message| message.id)
.collect();
let buffer = self.buffer.read(cx).snapshot();
let invalidated_caches: HashSet<MessageId> = messages
let cache_deltas: HashSet<MessageId> = self
.messages_metadata
.iter()
.scan(false, |encountered_invalid, message| {
let message_id = message.id;
let is_invalid = self
.messages_metadata
.get(&message_id)
.map_or(true, |metadata| {
!metadata.is_cache_valid(&buffer, &message.offset_range)
|| *encountered_invalid
});
*encountered_invalid |= is_invalid;
Some(if is_invalid { Some(message_id) } else { None })
.filter_map(|(id, metadata)| {
let should_cache = longest_message_ids.contains(id);
let should_be_anchor = should_cache && cache_configuration.max_cache_anchors > 0;
if metadata.should_cache != should_cache
|| metadata.is_cache_anchor != should_be_anchor
{
Some(*id)
} else {
None
}
})
.flatten()
.collect();
let last_anchor = messages.iter().rev().find_map(|message| {
if anchors.contains(&message.id) {
Some(message.id)
} else {
None
}
});
let mut new_anchor_needs_caching = false;
let current_version = &buffer.version;
// If we have no anchors, mark all messages as not being cached.
let mut hit_last_anchor = last_anchor.is_none();
for message in messages.iter() {
if hit_last_anchor {
self.update_metadata(message.id, cx, |metadata| metadata.cache = None);
continue;
}
if let Some(last_anchor) = last_anchor {
if message.id == last_anchor {
hit_last_anchor = true;
}
}
new_anchor_needs_caching = new_anchor_needs_caching
|| (invalidated_caches.contains(&message.id) && anchors.contains(&message.id));
self.update_metadata(message.id, cx, |metadata| {
let cache_status = if invalidated_caches.contains(&message.id) {
CacheStatus::Pending
} else {
metadata
.cache
.as_ref()
.map_or(CacheStatus::Pending, |cm| cm.status.clone())
};
metadata.cache = Some(MessageCacheMetadata {
is_anchor: anchors.contains(&message.id),
is_final_anchor: hit_last_anchor,
status: cache_status,
cached_at: current_version.clone(),
});
let mut newly_cached_item = false;
for id in cache_deltas {
newly_cached_item = newly_cached_item || longest_message_ids.contains(&id);
self.update_metadata(id, cx, |metadata| {
metadata.should_cache = longest_message_ids.contains(&id);
metadata.is_cache_anchor =
metadata.should_cache && (cache_configuration.max_cache_anchors > 0);
});
}
new_anchor_needs_caching
newly_cached_item
}
fn start_cache_warming(&mut self, model: &Arc<dyn LanguageModel>, cx: &mut ModelContext<Self>) {
let cache_configuration = model.cache_configuration();
if !self.mark_cache_anchors(&cache_configuration, true, cx) {
return;
}
if !self.pending_completions.is_empty() {
if !self.mark_longest_messages_for_cache(&cache_configuration, true, cx) {
return;
}
if let Some(cache_configuration) = cache_configuration {
@@ -1142,7 +1074,7 @@ impl Context {
};
let model = Arc::clone(model);
self.pending_cache_warming_task = cx.spawn(|this, mut cx| {
self.pending_cache_warming_task = cx.spawn(|_, cx| {
async move {
match model.stream_completion(request, &cx).await {
Ok(mut stream) => {
@@ -1153,41 +1085,13 @@ impl Context {
log::warn!("Cache warming failed: {}", e);
}
};
this.update(&mut cx, |this, cx| {
this.update_cache_status_for_completion(cx);
})
.ok();
anyhow::Ok(())
}
.log_err()
});
}
pub fn update_cache_status_for_completion(&mut self, cx: &mut ModelContext<Self>) {
let cached_message_ids: Vec<MessageId> = self
.messages_metadata
.iter()
.filter_map(|(message_id, metadata)| {
metadata.cache.as_ref().and_then(|cache| {
if cache.status == CacheStatus::Pending {
Some(*message_id)
} else {
None
}
})
})
.collect();
for message_id in cached_message_ids {
self.update_metadata(message_id, cx, |metadata| {
if let Some(cache) = &mut metadata.cache {
cache.status = CacheStatus::Cached;
}
});
}
cx.notify();
}
pub fn reparse_slash_commands(&mut self, cx: &mut ModelContext<Self>) {
let buffer = self.buffer.read(cx);
let mut row_ranges = self
@@ -1491,8 +1395,7 @@ impl Context {
&mut self,
command_range: Range<language::Anchor>,
output: Task<Result<SlashCommandOutput>>,
ensure_trailing_newline: bool,
expand_result: bool,
insert_trailing_newline: bool,
cx: &mut ModelContext<Self>,
) {
self.reparse_slash_commands(cx);
@@ -1503,27 +1406,8 @@ impl Context {
let output = output.await;
this.update(&mut cx, |this, cx| match output {
Ok(mut output) => {
// Ensure section ranges are valid.
for section in &mut output.sections {
section.range.start = section.range.start.min(output.text.len());
section.range.end = section.range.end.min(output.text.len());
while !output.text.is_char_boundary(section.range.start) {
section.range.start -= 1;
}
while !output.text.is_char_boundary(section.range.end) {
section.range.end += 1;
}
}
// Ensure there is a newline after the last section.
if ensure_trailing_newline {
let has_newline_after_last_section =
output.sections.last().map_or(false, |last_section| {
output.text[last_section.range.end..].ends_with('\n')
});
if !has_newline_after_last_section {
output.text.push('\n');
}
if insert_trailing_newline {
output.text.push('\n');
}
let version = this.version.clone();
@@ -1566,7 +1450,6 @@ impl Context {
output_range,
sections,
run_commands_in_output: output.run_commands_in_text,
expand_result,
},
)
});
@@ -1625,7 +1508,7 @@ impl Context {
return None;
}
// Compute which messages to cache, including the last one.
self.mark_cache_anchors(&model.cache_configuration(), false, cx);
self.mark_longest_messages_for_cache(&model.cache_configuration(), false, cx);
let request = self.to_completion_request(cx);
let assistant_message = self
@@ -1690,7 +1573,6 @@ impl Context {
this.pending_completions
.retain(|completion| completion.id != pending_completion_id);
this.summarize(false, cx);
this.update_cache_status_for_completion(cx);
})?;
anyhow::Ok(())
@@ -1841,7 +1723,8 @@ impl Context {
role,
status,
timestamp: anchor.id.0,
cache: None,
should_cache: false,
is_cache_anchor: false,
};
self.insert_message(anchor.clone(), metadata.clone(), cx);
self.push_op(
@@ -1958,7 +1841,8 @@ impl Context {
role,
status: MessageStatus::Done,
timestamp: suffix.id.0,
cache: None,
should_cache: false,
is_cache_anchor: false,
};
self.insert_message(suffix.clone(), suffix_metadata.clone(), cx);
self.push_op(
@@ -2008,7 +1892,8 @@ impl Context {
role,
status: MessageStatus::Done,
timestamp: selection.id.0,
cache: None,
should_cache: false,
is_cache_anchor: false,
};
self.insert_message(selection.clone(), selection_metadata.clone(), cx);
self.push_op(
@@ -2242,7 +2127,7 @@ impl Context {
anchor: message_anchor.start,
role: metadata.role,
status: metadata.status.clone(),
cache: metadata.cache.clone(),
cache: metadata.is_cache_anchor,
image_offsets,
});
}
@@ -2489,7 +2374,8 @@ impl SavedContext {
role: message.metadata.role,
status: message.metadata.status,
timestamp: message.metadata.timestamp,
cache: None,
should_cache: false,
is_cache_anchor: false,
},
version: version.clone(),
});
@@ -2506,7 +2392,8 @@ impl SavedContext {
role: metadata.role,
status: metadata.status,
timestamp,
cache: None,
should_cache: false,
is_cache_anchor: false,
},
version: version.clone(),
});
@@ -2601,7 +2488,8 @@ impl SavedContextV0_3_0 {
role: metadata.role,
status: metadata.status.clone(),
timestamp,
cache: None,
should_cache: false,
is_cache_anchor: false,
},
image_offsets: Vec::new(),
})

View File

@@ -1,6 +1,6 @@
use crate::{
assistant_panel, prompt_library, slash_command::file_command, workflow::tool, CacheStatus,
Context, ContextEvent, ContextId, ContextOperation, MessageId, MessageStatus, PromptBuilder,
assistant_panel, prompt_library, slash_command::file_command, workflow::tool, Context,
ContextEvent, ContextId, ContextOperation, MessageId, MessageStatus, PromptBuilder,
};
use anyhow::Result;
use assistant_slash_command::{
@@ -12,7 +12,7 @@ use fs::{FakeFs, Fs as _};
use gpui::{AppContext, Model, SharedString, Task, TestAppContext, WeakView};
use indoc::indoc;
use language::{Buffer, LanguageRegistry, LspAdapterDelegate};
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
use language_model::{LanguageModelRegistry, Role};
use parking_lot::Mutex;
use project::Project;
use rand::prelude::*;
@@ -33,8 +33,6 @@ use unindent::Unindent;
use util::{test::marked_text_ranges, RandomCharIter};
use workspace::Workspace;
use super::MessageCacheMetadata;
#[gpui::test]
fn test_inserting_and_removing_messages(cx: &mut AppContext) {
let settings_store = SettingsStore::test(cx);
@@ -475,7 +473,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
}
#[gpui::test]
async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
async fn test_edit_step_parsing(cx: &mut TestAppContext) {
cx.update(prompt_library::init);
let settings_store = cx.update(SettingsStore::test);
cx.set_global(settings_store);
@@ -893,7 +891,6 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
run_commands_in_text: false,
})),
true,
false,
cx,
);
});
@@ -1004,159 +1001,6 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
});
}
#[gpui::test]
fn test_mark_cache_anchors(cx: &mut AppContext) {
let settings_store = SettingsStore::test(cx);
LanguageModelRegistry::test(cx);
cx.set_global(settings_store);
assistant_panel::init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context =
cx.new_model(|cx| Context::local(registry, None, None, prompt_builder.clone(), cx));
let buffer = context.read(cx).buffer.clone();
// Create a test cache configuration
let cache_configuration = &Some(LanguageModelCacheConfiguration {
max_cache_anchors: 3,
should_speculate: true,
min_total_token: 10,
});
let message_1 = context.read(cx).message_anchors[0].clone();
context.update(cx, |context, cx| {
context.mark_cache_anchors(cache_configuration, false, cx)
});
assert_eq!(
messages_cache(&context, cx)
.iter()
.filter(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor))
.count(),
0,
"Empty messages should not have any cache anchors."
);
buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
let message_2 = context
.update(cx, |context, cx| {
context.insert_message_after(message_1.id, Role::User, MessageStatus::Pending, cx)
})
.unwrap();
buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbbbbbb")], None, cx));
let message_3 = context
.update(cx, |context, cx| {
context.insert_message_after(message_2.id, Role::User, MessageStatus::Pending, cx)
})
.unwrap();
buffer.update(cx, |buffer, cx| buffer.edit([(12..12, "cccccc")], None, cx));
context.update(cx, |context, cx| {
context.mark_cache_anchors(cache_configuration, false, cx)
});
assert_eq!(buffer.read(cx).text(), "aaa\nbbbbbbb\ncccccc");
assert_eq!(
messages_cache(&context, cx)
.iter()
.filter(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor))
.count(),
0,
"Messages should not be marked for cache before going over the token minimum."
);
context.update(cx, |context, _| {
context.token_count = Some(20);
});
context.update(cx, |context, cx| {
context.mark_cache_anchors(cache_configuration, true, cx)
});
assert_eq!(
messages_cache(&context, cx)
.iter()
.map(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor))
.collect::<Vec<bool>>(),
vec![true, true, false],
"Last message should not be an anchor on speculative request."
);
context
.update(cx, |context, cx| {
context.insert_message_after(message_3.id, Role::Assistant, MessageStatus::Pending, cx)
})
.unwrap();
context.update(cx, |context, cx| {
context.mark_cache_anchors(cache_configuration, false, cx)
});
assert_eq!(
messages_cache(&context, cx)
.iter()
.map(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor))
.collect::<Vec<bool>>(),
vec![false, true, true, false],
"Most recent message should also be cached if not a speculative request."
);
context.update(cx, |context, cx| {
context.update_cache_status_for_completion(cx)
});
assert_eq!(
messages_cache(&context, cx)
.iter()
.map(|(_, cache)| cache
.as_ref()
.map_or(None, |cache| Some(cache.status.clone())))
.collect::<Vec<Option<CacheStatus>>>(),
vec![
Some(CacheStatus::Cached),
Some(CacheStatus::Cached),
Some(CacheStatus::Cached),
None
],
"All user messages prior to anchor should be marked as cached."
);
buffer.update(cx, |buffer, cx| buffer.edit([(14..14, "d")], None, cx));
context.update(cx, |context, cx| {
context.mark_cache_anchors(cache_configuration, false, cx)
});
assert_eq!(
messages_cache(&context, cx)
.iter()
.map(|(_, cache)| cache
.as_ref()
.map_or(None, |cache| Some(cache.status.clone())))
.collect::<Vec<Option<CacheStatus>>>(),
vec![
Some(CacheStatus::Cached),
Some(CacheStatus::Cached),
Some(CacheStatus::Pending),
None
],
"Modifying a message should invalidate it's cache but leave previous messages."
);
buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "e")], None, cx));
context.update(cx, |context, cx| {
context.mark_cache_anchors(cache_configuration, false, cx)
});
assert_eq!(
messages_cache(&context, cx)
.iter()
.map(|(_, cache)| cache
.as_ref()
.map_or(None, |cache| Some(cache.status.clone())))
.collect::<Vec<Option<CacheStatus>>>(),
vec![
Some(CacheStatus::Pending),
Some(CacheStatus::Pending),
Some(CacheStatus::Pending),
None
],
"Modifying a message should invalidate all future messages."
);
}
fn messages(context: &Model<Context>, cx: &AppContext) -> Vec<(MessageId, Role, Range<usize>)> {
context
.read(cx)
@@ -1165,17 +1009,6 @@ fn messages(context: &Model<Context>, cx: &AppContext) -> Vec<(MessageId, Role,
.collect()
}
fn messages_cache(
context: &Model<Context>,
cx: &AppContext,
) -> Vec<(MessageId, Option<MessageCacheMetadata>)> {
context
.read(cx)
.messages(cx)
.map(|message| (message.id, message.cache.clone()))
.collect()
}
#[derive(Clone)]
struct FakeSlashCommand(String);

View File

@@ -28,7 +28,7 @@ use gpui::{
FontWeight, Global, HighlightStyle, Model, ModelContext, Subscription, Task, TextStyle,
UpdateGlobal, View, ViewContext, WeakView, WindowContext,
};
use language::{Buffer, IndentKind, Point, Selection, TransactionId};
use language::{Buffer, IndentKind, Point, TransactionId};
use language_model::{
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
};
@@ -38,7 +38,6 @@ use rope::Rope;
use settings::Settings;
use smol::future::FutureExt;
use std::{
cmp,
future::{self, Future},
mem,
ops::{Range, RangeInclusive},
@@ -47,6 +46,7 @@ use std::{
task::{self, Poll},
time::{Duration, Instant},
};
use text::OffsetRangeExt as _;
use theme::ThemeSettings;
use ui::{prelude::*, CheckboxWithLabel, IconButtonShape, Popover, Tooltip};
use util::{RangeExt, ResultExt};
@@ -76,13 +76,8 @@ 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>,
),
>,
assist_observations:
HashMap<InlineAssistId, (async_watch::Sender<()>, async_watch::Receiver<()>)>,
confirmed_assists: HashMap<InlineAssistId, Model<Codegen>>,
prompt_history: VecDeque<String>,
prompt_builder: Arc<PromptBuilder>,
@@ -90,19 +85,6 @@ 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 {
@@ -158,66 +140,81 @@ impl InlineAssistant {
cx: &mut WindowContext,
) {
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
struct CodegenRange {
transform_range: Range<Point>,
selection_ranges: Vec<Range<Point>>,
focus_assist: bool,
}
let mut selections = Vec::<Selection<Point>>::new();
let mut newest_selection = None;
for mut selection in editor.read(cx).selections.all::<Point>(cx) {
if selection.end > selection.start {
selection.start.column = 0;
// If the selection ends at the start of the line, we don't want to include it.
if selection.end.column == 0 {
selection.end.row -= 1;
}
selection.end.column = snapshot.line_len(MultiBufferRow(selection.end.row));
let newest_selection_range = editor.read(cx).selections.newest::<Point>(cx).range();
let mut codegen_ranges: Vec<CodegenRange> = Vec::new();
let selection_ranges = snapshot
.split_ranges(editor.read(cx).selections.disjoint_anchor_ranges())
.map(|range| range.to_point(&snapshot))
.collect::<Vec<Range<Point>>>();
for selection_range in selection_ranges {
let selection_is_newest = newest_selection_range.contains_inclusive(&selection_range);
let mut transform_range = selection_range.start..selection_range.end;
// Expand the transform range to start/end of lines.
// If a non-empty selection ends at the start of the last line, clip at the end of the penultimate line.
transform_range.start.column = 0;
if transform_range.end.column == 0 && transform_range.end > transform_range.start {
transform_range.end.row -= 1;
}
transform_range.end.column = snapshot.line_len(MultiBufferRow(transform_range.end.row));
let selection_range =
selection_range.start..selection_range.end.min(transform_range.end);
if let Some(prev_selection) = selections.last_mut() {
if selection.start <= prev_selection.end {
prev_selection.end = selection.end;
// If we intersect the previous transform range,
if let Some(CodegenRange {
transform_range: prev_transform_range,
selection_ranges,
focus_assist,
}) = codegen_ranges.last_mut()
{
if transform_range.start <= prev_transform_range.end {
prev_transform_range.end = transform_range.end;
selection_ranges.push(selection_range);
*focus_assist |= selection_is_newest;
continue;
}
}
let latest_selection = newest_selection.get_or_insert_with(|| selection.clone());
if selection.id > latest_selection.id {
*latest_selection = selection.clone();
}
selections.push(selection);
}
let newest_selection = newest_selection.unwrap();
let mut codegen_ranges = Vec::new();
for (excerpt_id, buffer, buffer_range) in
snapshot.excerpts_in_ranges(selections.iter().map(|selection| {
snapshot.anchor_before(selection.start)..snapshot.anchor_after(selection.end)
}))
{
let start = Anchor {
buffer_id: Some(buffer.remote_id()),
excerpt_id,
text_anchor: buffer.anchor_before(buffer_range.start),
};
let end = Anchor {
buffer_id: Some(buffer.remote_id()),
excerpt_id,
text_anchor: buffer.anchor_after(buffer_range.end),
};
codegen_ranges.push(start..end);
codegen_ranges.push(CodegenRange {
transform_range,
selection_ranges: vec![selection_range],
focus_assist: selection_is_newest,
})
}
let assist_group_id = self.next_assist_group_id.post_inc();
let prompt_buffer =
cx.new_model(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx));
let prompt_buffer = cx.new_model(|cx| MultiBuffer::singleton(prompt_buffer, cx));
let mut assists = Vec::new();
let mut assist_to_focus = None;
for range in codegen_ranges {
let assist_id = self.next_assist_id.post_inc();
for CodegenRange {
transform_range,
selection_ranges,
focus_assist,
} in codegen_ranges
{
let transform_range = snapshot.anchor_before(transform_range.start)
..snapshot.anchor_after(transform_range.end);
let selection_ranges = selection_ranges
.iter()
.map(|range| snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end))
.collect::<Vec<_>>();
let codegen = cx.new_model(|cx| {
Codegen::new(
editor.read(cx).buffer().clone(),
range.clone(),
transform_range.clone(),
selection_ranges,
None,
self.telemetry.clone(),
self.prompt_builder.clone(),
@@ -225,6 +222,7 @@ impl InlineAssistant {
)
});
let assist_id = self.next_assist_id.post_inc();
let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default()));
let prompt_editor = cx.new_view(|cx| {
PromptEditor::new(
@@ -241,23 +239,16 @@ impl InlineAssistant {
)
});
if assist_to_focus.is_none() {
let focus_assist = if newest_selection.reversed {
range.start.to_point(&snapshot) == newest_selection.start
} else {
range.end.to_point(&snapshot) == newest_selection.end
};
if focus_assist {
assist_to_focus = Some(assist_id);
}
if focus_assist {
assist_to_focus = Some(assist_id);
}
let [prompt_block_id, end_block_id] =
self.insert_assist_blocks(editor, &range, &prompt_editor, cx);
self.insert_assist_blocks(editor, &transform_range, &prompt_editor, cx);
assists.push((
assist_id,
range,
transform_range,
prompt_editor,
prompt_block_id,
end_block_id,
@@ -324,6 +315,7 @@ impl InlineAssistant {
Codegen::new(
editor.read(cx).buffer().clone(),
range.clone(),
vec![range.clone()],
initial_transaction_id,
self.telemetry.clone(),
self.prompt_builder.clone(),
@@ -933,17 +925,12 @@ impl InlineAssistant {
assist
.codegen
.update(cx, |codegen, cx| {
codegen.start(
assist.range.clone(),
user_prompt,
assistant_panel_context,
cx,
)
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();
tx.send(()).ok();
}
}
@@ -957,7 +944,7 @@ 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();
tx.send(()).ok();
}
}
@@ -1159,14 +1146,11 @@ impl InlineAssistant {
})
}
pub fn observe_assist(
&mut self,
assist_id: InlineAssistId,
) -> async_watch::Receiver<AssistStatus> {
pub fn observe_assist(&mut self, assist_id: InlineAssistId) -> async_watch::Receiver<()> {
if let Some((_, rx)) = self.assist_observations.get(&assist_id) {
rx.clone()
} else {
let (tx, rx) = async_watch::channel(AssistStatus::Idle);
let (tx, rx) = async_watch::channel(());
self.assist_observations.insert(assist_id, (tx, rx.clone()));
rx
}
@@ -2100,7 +2084,7 @@ 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();
tx.0.send(()).ok();
}
}
})
@@ -2136,12 +2120,9 @@ impl InlineAssist {
return future::ready(Err(anyhow!("no user prompt"))).boxed();
};
let assistant_panel_context = self.assistant_panel_context(cx);
self.codegen.read(cx).count_tokens(
self.range.clone(),
user_prompt,
assistant_panel_context,
cx,
)
self.codegen
.read(cx)
.count_tokens(user_prompt, assistant_panel_context, cx)
}
}
@@ -2162,6 +2143,8 @@ pub struct Codegen {
buffer: Model<MultiBuffer>,
old_buffer: Model<Buffer>,
snapshot: MultiBufferSnapshot,
transform_range: Range<Anchor>,
selected_ranges: Vec<Range<Anchor>>,
edit_position: Option<Anchor>,
last_equal_ranges: Vec<Range<Anchor>>,
initial_transaction_id: Option<TransactionId>,
@@ -2171,7 +2154,7 @@ pub struct Codegen {
diff: Diff,
telemetry: Option<Arc<Telemetry>>,
_subscription: gpui::Subscription,
builder: Arc<PromptBuilder>,
prompt_builder: Arc<PromptBuilder>,
}
enum CodegenStatus {
@@ -2198,7 +2181,8 @@ impl EventEmitter<CodegenEvent> for Codegen {}
impl Codegen {
pub fn new(
buffer: Model<MultiBuffer>,
range: Range<Anchor>,
transform_range: Range<Anchor>,
selected_ranges: Vec<Range<Anchor>>,
initial_transaction_id: Option<TransactionId>,
telemetry: Option<Arc<Telemetry>>,
builder: Arc<PromptBuilder>,
@@ -2208,7 +2192,7 @@ impl Codegen {
let (old_buffer, _, _) = buffer
.read(cx)
.range_to_buffer_ranges(range.clone(), cx)
.range_to_buffer_ranges(transform_range.clone(), cx)
.pop()
.unwrap();
let old_buffer = cx.new_model(|cx| {
@@ -2239,7 +2223,9 @@ impl Codegen {
telemetry,
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
initial_transaction_id,
builder,
prompt_builder: builder,
transform_range,
selected_ranges,
}
}
@@ -2264,14 +2250,12 @@ impl Codegen {
pub fn count_tokens(
&self,
edit_range: Range<Anchor>,
user_prompt: String,
assistant_panel_context: Option<LanguageModelRequest>,
cx: &AppContext,
) -> BoxFuture<'static, Result<TokenCounts>> {
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
let request =
self.build_request(user_prompt, assistant_panel_context.clone(), edit_range, cx);
let request = self.build_request(user_prompt, assistant_panel_context.clone(), cx);
match request {
Ok(request) => {
let total_count = model.count_tokens(request.clone(), cx);
@@ -2296,7 +2280,6 @@ impl Codegen {
pub fn start(
&mut self,
edit_range: Range<Anchor>,
user_prompt: String,
assistant_panel_context: Option<LanguageModelRequest>,
cx: &mut ModelContext<Self>,
@@ -2311,24 +2294,20 @@ impl Codegen {
});
}
self.edit_position = Some(edit_range.start.bias_right(&self.snapshot));
self.edit_position = Some(self.transform_range.start.bias_right(&self.snapshot));
let telemetry_id = model.telemetry_id();
let chunks: LocalBoxFuture<Result<BoxStream<Result<String>>>> = if user_prompt
.trim()
.to_lowercase()
== "delete"
{
async { Ok(stream::empty().boxed()) }.boxed_local()
} else {
let request =
self.build_request(user_prompt, assistant_panel_context, edit_range.clone(), cx)?;
let chunks: LocalBoxFuture<Result<BoxStream<Result<String>>>> =
if user_prompt.trim().to_lowercase() == "delete" {
async { Ok(stream::empty().boxed()) }.boxed_local()
} else {
let request = self.build_request(user_prompt, assistant_panel_context, cx)?;
let chunks =
cx.spawn(|_, cx| async move { model.stream_completion(request, &cx).await });
async move { Ok(chunks.await?.boxed()) }.boxed_local()
};
self.handle_stream(telemetry_id, edit_range, chunks, cx);
let chunks =
cx.spawn(|_, cx| async move { model.stream_completion(request, &cx).await });
async move { Ok(chunks.await?.boxed()) }.boxed_local()
};
self.handle_stream(telemetry_id, self.transform_range.clone(), chunks, cx);
Ok(())
}
@@ -2336,11 +2315,10 @@ impl Codegen {
&self,
user_prompt: String,
assistant_panel_context: Option<LanguageModelRequest>,
edit_range: Range<Anchor>,
cx: &AppContext,
) -> Result<LanguageModelRequest> {
let buffer = self.buffer.read(cx).snapshot(cx);
let language = buffer.language_at(edit_range.start);
let language = buffer.language_at(self.transform_range.start);
let language_name = if let Some(language) = language.as_ref() {
if Arc::ptr_eq(language, &language::PLAIN_TEXT) {
None
@@ -2365,9 +2343,9 @@ impl Codegen {
};
let language_name = language_name.as_deref();
let start = buffer.point_to_buffer_offset(edit_range.start);
let end = buffer.point_to_buffer_offset(edit_range.end);
let (buffer, range) = if let Some((start, end)) = start.zip(end) {
let start = buffer.point_to_buffer_offset(self.transform_range.start);
let end = buffer.point_to_buffer_offset(self.transform_range.end);
let (transform_buffer, transform_range) = if let Some((start, end)) = start.zip(end) {
let (start_buffer, start_buffer_offset) = start;
let (end_buffer, end_buffer_offset) = end;
if start_buffer.remote_id() == end_buffer.remote_id() {
@@ -2379,9 +2357,39 @@ impl Codegen {
return Err(anyhow::anyhow!("invalid transformation range"));
};
let mut transform_context_range = transform_range.to_point(&transform_buffer);
transform_context_range.start.row = transform_context_range.start.row.saturating_sub(3);
transform_context_range.start.column = 0;
transform_context_range.end =
(transform_context_range.end + Point::new(3, 0)).min(transform_buffer.max_point());
transform_context_range.end.column =
transform_buffer.line_len(transform_context_range.end.row);
let transform_context_range = transform_context_range.to_offset(&transform_buffer);
let selected_ranges = self
.selected_ranges
.iter()
.filter_map(|selected_range| {
let start = buffer
.point_to_buffer_offset(selected_range.start)
.map(|(_, offset)| offset)?;
let end = buffer
.point_to_buffer_offset(selected_range.end)
.map(|(_, offset)| offset)?;
Some(start..end)
})
.collect::<Vec<_>>();
let prompt = self
.builder
.generate_content_prompt(user_prompt, language_name, buffer, range)
.prompt_builder
.generate_content_prompt(
user_prompt,
language_name,
transform_buffer,
transform_range,
selected_ranges,
transform_context_range,
)
.map_err(|e| anyhow::anyhow!("Failed to generate content prompt: {}", e))?;
let mut messages = Vec::new();
@@ -2454,84 +2462,19 @@ impl Codegen {
let mut diff = StreamingDiff::new(selected_text.to_string());
let mut line_diff = LineDiff::default();
let mut new_text = String::new();
let mut base_indent = None;
let mut line_indent = None;
let mut first_line = true;
while let Some(chunk) = chunks.next().await {
if response_latency.is_none() {
response_latency = Some(request_start.elapsed());
}
let chunk = chunk?;
let mut lines = chunk.split('\n').peekable();
while let Some(line) = lines.next() {
new_text.push_str(line);
if line_indent.is_none() {
if let Some(non_whitespace_ch_ix) =
new_text.find(|ch: char| !ch.is_whitespace())
{
line_indent = Some(non_whitespace_ch_ix);
base_indent = base_indent.or(line_indent);
let line_indent = line_indent.unwrap();
let base_indent = base_indent.unwrap();
let indent_delta =
line_indent as i32 - base_indent as i32;
let mut corrected_indent_len = cmp::max(
0,
suggested_line_indent.len as i32 + indent_delta,
)
as usize;
if first_line {
corrected_indent_len = corrected_indent_len
.saturating_sub(
selection_start.column as usize,
);
}
let indent_char = suggested_line_indent.char();
let mut indent_buffer = [0; 4];
let indent_str =
indent_char.encode_utf8(&mut indent_buffer);
new_text.replace_range(
..line_indent,
&indent_str.repeat(corrected_indent_len),
);
}
}
if line_indent.is_some() {
let char_ops = diff.push_new(&new_text);
line_diff
.push_char_operations(&char_ops, &selected_text);
diff_tx
.send((char_ops, line_diff.line_operations()))
.await?;
new_text.clear();
}
if lines.peek().is_some() {
let char_ops = diff.push_new("\n");
line_diff
.push_char_operations(&char_ops, &selected_text);
diff_tx
.send((char_ops, line_diff.line_operations()))
.await?;
if line_indent.is_none() {
// Don't write out the leading indentation in empty lines on the next line
// This is the case where the above if statement didn't clear the buffer
new_text.clear();
}
line_indent = None;
first_line = false;
}
}
let char_ops = diff.push_new(&chunk);
line_diff.push_char_operations(&char_ops, &selected_text);
diff_tx
.send((char_ops, line_diff.line_operations()))
.await?;
}
let mut char_ops = diff.push_new(&new_text);
char_ops.extend(diff.finish());
let char_ops = diff.finish();
line_diff.push_char_operations(&char_ops, &selected_text);
line_diff.finish(&selected_text);
diff_tx
@@ -2995,311 +2938,13 @@ fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
mod tests {
use super::*;
use futures::stream::{self};
use gpui::{Context, TestAppContext};
use indoc::indoc;
use language::{
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
Point,
};
use language_model::LanguageModelRegistry;
use rand::prelude::*;
use serde::Serialize;
use settings::SettingsStore;
use std::{future, sync::Arc};
#[derive(Serialize)]
pub struct DummyCompletionRequest {
pub name: String,
}
#[gpui::test(iterations = 10)]
async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
cx.set_global(cx.update(SettingsStore::test));
cx.update(language_model::LanguageModelRegistry::test);
cx.update(language_settings::init);
let text = indoc! {"
fn main() {
let x = 0;
for _ in 0..10 {
x += 1;
}
}
"};
let buffer =
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let range = buffer.read_with(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let codegen = cx.new_model(|cx| {
Codegen::new(
buffer.clone(),
range.clone(),
None,
None,
prompt_builder,
cx,
)
});
let (chunks_tx, chunks_rx) = mpsc::unbounded();
codegen.update(cx, |codegen, cx| {
codegen.handle_stream(
String::new(),
range,
future::ready(Ok(chunks_rx.map(|chunk| Ok(chunk)).boxed())),
cx,
)
});
let mut new_text = concat!(
" let mut x = 0;\n",
" while x < 10 {\n",
" x += 1;\n",
" }",
);
while !new_text.is_empty() {
let max_len = cmp::min(new_text.len(), 10);
let len = rng.gen_range(1..=max_len);
let (chunk, suffix) = new_text.split_at(len);
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
new_text = suffix;
cx.background_executor.run_until_parked();
}
drop(chunks_tx);
cx.background_executor.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
indoc! {"
fn main() {
let mut x = 0;
while x < 10 {
x += 1;
}
}
"}
);
}
#[gpui::test(iterations = 10)]
async fn test_autoindent_when_generating_past_indentation(
cx: &mut TestAppContext,
mut rng: StdRng,
) {
cx.set_global(cx.update(SettingsStore::test));
cx.update(language_settings::init);
let text = indoc! {"
fn main() {
le
}
"};
let buffer =
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let range = buffer.read_with(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
snapshot.anchor_before(Point::new(1, 6))..snapshot.anchor_after(Point::new(1, 6))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let codegen = cx.new_model(|cx| {
Codegen::new(
buffer.clone(),
range.clone(),
None,
None,
prompt_builder,
cx,
)
});
let (chunks_tx, chunks_rx) = mpsc::unbounded();
codegen.update(cx, |codegen, cx| {
codegen.handle_stream(
String::new(),
range.clone(),
future::ready(Ok(chunks_rx.map(|chunk| Ok(chunk)).boxed())),
cx,
)
});
cx.background_executor.run_until_parked();
let mut new_text = concat!(
"t mut x = 0;\n",
"while x < 10 {\n",
" x += 1;\n",
"}", //
);
while !new_text.is_empty() {
let max_len = cmp::min(new_text.len(), 10);
let len = rng.gen_range(1..=max_len);
let (chunk, suffix) = new_text.split_at(len);
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
new_text = suffix;
cx.background_executor.run_until_parked();
}
drop(chunks_tx);
cx.background_executor.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
indoc! {"
fn main() {
let mut x = 0;
while x < 10 {
x += 1;
}
}
"}
);
}
#[gpui::test(iterations = 10)]
async fn test_autoindent_when_generating_before_indentation(
cx: &mut TestAppContext,
mut rng: StdRng,
) {
cx.update(LanguageModelRegistry::test);
cx.set_global(cx.update(SettingsStore::test));
cx.update(language_settings::init);
let text = concat!(
"fn main() {\n",
" \n",
"}\n" //
);
let buffer =
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let range = buffer.read_with(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
snapshot.anchor_before(Point::new(1, 2))..snapshot.anchor_after(Point::new(1, 2))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let codegen = cx.new_model(|cx| {
Codegen::new(
buffer.clone(),
range.clone(),
None,
None,
prompt_builder,
cx,
)
});
let (chunks_tx, chunks_rx) = mpsc::unbounded();
codegen.update(cx, |codegen, cx| {
codegen.handle_stream(
String::new(),
range.clone(),
future::ready(Ok(chunks_rx.map(|chunk| Ok(chunk)).boxed())),
cx,
)
});
cx.background_executor.run_until_parked();
let mut new_text = concat!(
"let mut x = 0;\n",
"while x < 10 {\n",
" x += 1;\n",
"}", //
);
while !new_text.is_empty() {
let max_len = cmp::min(new_text.len(), 10);
let len = rng.gen_range(1..=max_len);
let (chunk, suffix) = new_text.split_at(len);
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
new_text = suffix;
cx.background_executor.run_until_parked();
}
drop(chunks_tx);
cx.background_executor.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
indoc! {"
fn main() {
let mut x = 0;
while x < 10 {
x += 1;
}
}
"}
);
}
#[gpui::test(iterations = 10)]
async fn test_autoindent_respects_tabs_in_selection(cx: &mut TestAppContext) {
cx.update(LanguageModelRegistry::test);
cx.set_global(cx.update(SettingsStore::test));
cx.update(language_settings::init);
let text = indoc! {"
func main() {
\tx := 0
\tfor i := 0; i < 10; i++ {
\t\tx++
\t}
}
"};
let buffer = cx.new_model(|cx| Buffer::local(text, cx));
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let range = buffer.read_with(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(4, 2))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let codegen = cx.new_model(|cx| {
Codegen::new(
buffer.clone(),
range.clone(),
None,
None,
prompt_builder,
cx,
)
});
let (chunks_tx, chunks_rx) = mpsc::unbounded();
codegen.update(cx, |codegen, cx| {
codegen.handle_stream(
String::new(),
range.clone(),
future::ready(Ok(chunks_rx.map(|chunk| Ok(chunk)).boxed())),
cx,
)
});
let new_text = concat!(
"func main() {\n",
"\tx := 0\n",
"\tfor x < 10 {\n",
"\t\tx++\n",
"\t}", //
);
chunks_tx.unbounded_send(new_text.to_string()).unwrap();
drop(chunks_tx);
cx.background_executor.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
indoc! {"
func main() {
\tx := 0
\tfor x < 10 {
\t\tx++
\t}
}
"}
);
}
#[gpui::test]
async fn test_strip_invalid_spans_from_codeblock() {
assert_chunks("Lorem ipsum dolor", "Lorem ipsum dolor").await;
@@ -3339,27 +2984,4 @@ mod tests {
)
}
}
fn rust_lang() -> Language {
Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::language()),
)
.with_indents_query(
r#"
(call_expression) @indent
(field_expression) @indent
(_ "(" ")" @end) @indent
(_ "{" "}" @end) @indent
"#,
)
.unwrap()
}
}

View File

@@ -1,7 +1,6 @@
use feature_flags::ZedPro;
use gpui::Action;
use gpui::DismissEvent;
use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry};
use proto::Plan;
use workspace::ShowConfiguration;
@@ -37,7 +36,7 @@ pub struct ModelPickerDelegate {
#[derive(Clone)]
struct ModelInfo {
model: Arc<dyn LanguageModel>,
icon: IconName,
provider_icon: IconName,
availability: LanguageModelAvailability,
is_selected: bool,
}
@@ -150,8 +149,6 @@ impl PickerDelegate for ModelPickerDelegate {
use feature_flags::FeatureFlagAppExt;
let model_info = self.filtered_models.get(ix)?;
let show_badges = cx.has_flag::<ZedPro>();
let provider_name: String = model_info.model.provider_name().0.into();
Some(
ListItem::new(ix)
.inset(true)
@@ -159,7 +156,7 @@ impl PickerDelegate for ModelPickerDelegate {
.selected(selected)
.start_slot(
div().pr_1().child(
Icon::new(model_info.icon)
Icon::new(model_info.provider_icon)
.color(Color::Muted)
.size(IconSize::Medium),
),
@@ -169,16 +166,11 @@ impl PickerDelegate for ModelPickerDelegate {
.w_full()
.justify_between()
.font_buffer(cx)
.min_w(px(240.))
.min_w(px(200.))
.child(
h_flex()
.gap_2()
.child(Label::new(model_info.model.name().0.clone()))
.child(
Label::new(provider_name)
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.children(match model_info.availability {
LanguageModelAvailability::Public => None,
LanguageModelAvailability::RequiresPlan(Plan::Free) => None,
@@ -269,17 +261,16 @@ impl<T: PopoverTrigger> RenderOnce for ModelSelector<T> {
.iter()
.flat_map(|provider| {
let provider_id = provider.id();
let icon = provider.icon();
let provider_icon = provider.icon();
let selected_model = selected_model.clone();
let selected_provider = selected_provider.clone();
provider.provided_models(cx).into_iter().map(move |model| {
let model = model.clone();
let icon = model.icon().unwrap_or(icon);
ModelInfo {
model: model.clone(),
icon,
provider_icon,
availability: model.availability(),
is_selected: selected_model.as_ref() == Some(&model.id())
&& selected_provider.as_ref() == Some(&provider_id),

View File

@@ -926,7 +926,6 @@ impl PromptLibrary {
color: Some(cx.theme().status().predictive),
..HighlightStyle::default()
},
..EditorStyle::default()
},
)),
),

View File

@@ -1,24 +1,26 @@
use anyhow::Result;
use assets::Assets;
use fs::Fs;
use futures::StreamExt;
use gpui::AssetSource;
use handlebars::{Handlebars, RenderError};
use handlebars::{Handlebars, RenderError, TemplateError};
use language::BufferSnapshot;
use parking_lot::Mutex;
use serde::Serialize;
use std::{ops::Range, path::PathBuf, sync::Arc, time::Duration};
use std::{ops::Range, sync::Arc, time::Duration};
use util::ResultExt;
#[derive(Serialize)]
pub struct ContentPromptContext {
pub content_type: String,
pub language_name: Option<String>,
pub is_insert: bool,
pub is_truncated: bool,
pub document_content: String,
pub user_prompt: String,
pub rewrite_section: Option<String>,
pub rewrite_section: String,
pub rewrite_section_prefix: String,
pub rewrite_section_suffix: String,
pub rewrite_section_with_edits: String,
pub has_insertion: bool,
pub has_replacement: bool,
}
#[derive(Serialize)]
@@ -40,162 +42,128 @@ pub struct StepResolutionContext {
pub step_to_resolve: String,
}
pub struct PromptLoadingParams<'a> {
pub fs: Arc<dyn Fs>,
pub repo_path: Option<PathBuf>,
pub cx: &'a gpui::AppContext,
}
pub struct PromptBuilder {
handlebars: Arc<Mutex<Handlebars<'static>>>,
}
pub struct PromptOverrideContext<'a> {
pub dev_mode: bool,
pub fs: Arc<dyn Fs>,
pub cx: &'a mut gpui::AppContext,
}
impl PromptBuilder {
pub fn new(loading_params: Option<PromptLoadingParams>) -> Result<Self> {
pub fn new(override_cx: Option<PromptOverrideContext>) -> Result<Self, Box<TemplateError>> {
let mut handlebars = Handlebars::new();
Self::register_built_in_templates(&mut handlebars)?;
Self::register_templates(&mut handlebars)?;
let handlebars = Arc::new(Mutex::new(handlebars));
if let Some(params) = loading_params {
Self::watch_fs_for_template_overrides(params, handlebars.clone());
if let Some(override_cx) = override_cx {
Self::watch_fs_for_template_overrides(override_cx, handlebars.clone());
}
Ok(Self { handlebars })
}
/// Watches the filesystem for changes to prompt template overrides.
///
/// This function sets up a file watcher on the prompt templates directory. It performs
/// an initial scan of the directory and registers any existing template overrides.
/// Then it continuously monitors for changes, reloading templates as they are
/// modified or added.
///
/// If the templates directory doesn't exist initially, it waits for it to be created.
/// If the directory is removed, it restores the built-in templates and waits for the
/// directory to be recreated.
///
/// # Arguments
///
/// * `params` - A `PromptLoadingParams` struct containing the filesystem, repository path,
/// and application context.
/// * `handlebars` - An `Arc<Mutex<Handlebars>>` for registering and updating templates.
fn watch_fs_for_template_overrides(
mut params: PromptLoadingParams,
PromptOverrideContext { dev_mode, fs, cx }: PromptOverrideContext,
handlebars: Arc<Mutex<Handlebars<'static>>>,
) {
params.repo_path = None;
let templates_dir = paths::prompt_overrides_dir(params.repo_path.as_deref());
params.cx.background_executor()
cx.background_executor()
.spawn(async move {
let Some(parent_dir) = templates_dir.parent() else {
return;
let templates_dir = if dev_mode {
std::env::current_dir()
.ok()
.and_then(|pwd| {
let pwd_assets_prompts = pwd.join("assets").join("prompts");
pwd_assets_prompts.exists().then_some(pwd_assets_prompts)
})
.unwrap_or_else(|| paths::prompt_overrides_dir().clone())
} else {
paths::prompt_overrides_dir().clone()
};
let mut found_dir_once = false;
loop {
// Check if the templates directory exists and handle its status
// If it exists, log its presence and check if it's a symlink
// If it doesn't exist:
// - Log that we're using built-in prompts
// - Check if it's a broken symlink and log if so
// - Set up a watcher to detect when it's created
// After the first check, set the `found_dir_once` flag
// This allows us to avoid logging when looping back around after deleting the prompt overrides directory.
let dir_status = params.fs.is_dir(&templates_dir).await;
let symlink_status = params.fs.read_link(&templates_dir).await.ok();
if dir_status {
let mut log_message = format!("Prompt template overrides directory found at {}", templates_dir.display());
if let Some(target) = symlink_status {
log_message.push_str(" -> ");
log_message.push_str(&target.display().to_string());
}
log::info!("{}.", log_message);
} else {
if !found_dir_once {
log::info!("No prompt template overrides directory found at {}. Using built-in prompts.", templates_dir.display());
if let Some(target) = symlink_status {
log::info!("Symlink found pointing to {}, but target is invalid.", target.display());
}
}
if params.fs.is_dir(parent_dir).await {
let (mut changes, _watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
while let Some(changed_paths) = changes.next().await {
if changed_paths.iter().any(|p| p == &templates_dir) {
let mut log_message = format!("Prompt template overrides directory detected at {}", templates_dir.display());
if let Ok(target) = params.fs.read_link(&templates_dir).await {
log_message.push_str(" -> ");
log_message.push_str(&target.display().to_string());
}
log::info!("{}.", log_message);
break;
}
}
} else {
return;
}
// Create the prompt templates directory if it doesn't exist
if !fs.is_dir(&templates_dir).await {
if let Err(e) = fs.create_dir(&templates_dir).await {
log::error!("Failed to create prompt templates directory: {}", e);
return;
}
found_dir_once = true;
// Initial scan of the prompt overrides directory
if let Ok(mut entries) = params.fs.read_dir(&templates_dir).await {
while let Some(Ok(file_path)) = entries.next().await {
if file_path.to_string_lossy().ends_with(".hbs") {
if let Ok(content) = params.fs.load(&file_path).await {
let file_name = file_path.file_stem().unwrap().to_string_lossy();
log::info!("Registering prompt template override: {}", file_name);
handlebars.lock().register_template_string(&file_name, content).log_err();
}
}
}
}
// Watch both the parent directory and the template overrides directory:
// - Monitor the parent directory to detect if the template overrides directory is deleted.
// - Monitor the template overrides directory to re-register templates when they change.
// Combine both watch streams into a single stream.
let (parent_changes, parent_watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
let (changes, watcher) = params.fs.watch(&templates_dir, Duration::from_secs(1)).await;
let mut combined_changes = futures::stream::select(changes, parent_changes);
while let Some(changed_paths) = combined_changes.next().await {
if changed_paths.iter().any(|p| p == &templates_dir) {
if !params.fs.is_dir(&templates_dir).await {
log::info!("Prompt template overrides directory removed. Restoring built-in prompt templates.");
Self::register_built_in_templates(&mut handlebars.lock()).log_err();
break;
}
}
for changed_path in changed_paths {
if changed_path.starts_with(&templates_dir) && changed_path.extension().map_or(false, |ext| ext == "hbs") {
log::info!("Reloading prompt template override: {}", changed_path.display());
if let Some(content) = params.fs.load(&changed_path).await.log_err() {
let file_name = changed_path.file_stem().unwrap().to_string_lossy();
handlebars.lock().register_template_string(&file_name, content).log_err();
}
}
}
}
drop(watcher);
drop(parent_watcher);
}
// Initial scan of the prompts directory
if let Ok(mut entries) = fs.read_dir(&templates_dir).await {
while let Some(Ok(file_path)) = entries.next().await {
if file_path.to_string_lossy().ends_with(".hbs") {
if let Ok(content) = fs.load(&file_path).await {
let file_name = file_path.file_stem().unwrap().to_string_lossy();
match handlebars.lock().register_template_string(&file_name, content) {
Ok(_) => {
log::info!(
"Successfully registered template override: {} ({})",
file_name,
file_path.display()
);
},
Err(e) => {
log::error!(
"Failed to register template during initial scan: {} ({})",
e,
file_path.display()
);
},
}
}
}
}
}
// Watch for changes
let (mut changes, watcher) = fs.watch(&templates_dir, Duration::from_secs(1)).await;
while let Some(changed_paths) = changes.next().await {
for changed_path in changed_paths {
if changed_path.extension().map_or(false, |ext| ext == "hbs") {
log::info!("Reloading template: {}", changed_path.display());
if let Some(content) = fs.load(&changed_path).await.log_err() {
let file_name = changed_path.file_stem().unwrap().to_string_lossy();
let file_path = changed_path.to_string_lossy();
match handlebars.lock().register_template_string(&file_name, content) {
Ok(_) => log::info!(
"Successfully reloaded template: {} ({})",
file_name,
file_path
),
Err(e) => log::error!(
"Failed to register template: {} ({})",
e,
file_path
),
}
}
}
}
}
drop(watcher);
})
.detach();
}
fn register_built_in_templates(handlebars: &mut Handlebars) -> Result<()> {
for path in Assets.list("prompts")? {
if let Some(id) = path.split('/').last().and_then(|s| s.strip_suffix(".hbs")) {
if let Some(prompt) = Assets.load(path.as_ref()).log_err().flatten() {
log::info!("Registering built-in prompt template: {}", id);
handlebars
.register_template_string(id, String::from_utf8_lossy(prompt.as_ref()))?
}
}
}
fn register_templates(handlebars: &mut Handlebars) -> Result<(), Box<TemplateError>> {
let mut register_template = |id: &str| {
let prompt = Assets::get(&format!("prompts/{}.hbs", id))
.unwrap_or_else(|| panic!("{} prompt template not found", id))
.data;
handlebars
.register_template_string(id, String::from_utf8_lossy(&prompt))
.map_err(Box::new)
};
register_template("content_prompt")?;
register_template("terminal_assistant_prompt")?;
register_template("edit_workflow")?;
register_template("step_resolution")?;
Ok(())
}
@@ -205,7 +173,9 @@ impl PromptBuilder {
user_prompt: String,
language_name: Option<&str>,
buffer: BufferSnapshot,
range: Range<usize>,
transform_range: Range<usize>,
selected_ranges: Vec<Range<usize>>,
transform_context_range: Range<usize>,
) -> Result<String, RenderError> {
let content_type = match language_name {
None | Some("Markdown" | "Plain Text") => "text",
@@ -213,21 +183,20 @@ impl PromptBuilder {
};
const MAX_CTX: usize = 50000;
let is_insert = range.is_empty();
let mut is_truncated = false;
let before_range = 0..range.start;
let before_range = 0..transform_range.start;
let truncated_before = if before_range.len() > MAX_CTX {
is_truncated = true;
range.start - MAX_CTX..range.start
transform_range.start - MAX_CTX..transform_range.start
} else {
before_range
};
let after_range = range.end..buffer.len();
let after_range = transform_range.end..buffer.len();
let truncated_after = if after_range.len() > MAX_CTX {
is_truncated = true;
range.end..range.end + MAX_CTX
transform_range.end..transform_range.end + MAX_CTX
} else {
after_range
};
@@ -236,37 +205,74 @@ impl PromptBuilder {
for chunk in buffer.text_for_range(truncated_before) {
document_content.push_str(chunk);
}
if is_insert {
document_content.push_str("<insert_here></insert_here>");
} else {
document_content.push_str("<rewrite_this>\n");
for chunk in buffer.text_for_range(range.clone()) {
document_content.push_str(chunk);
}
document_content.push_str("\n</rewrite_this>");
document_content.push_str("<rewrite_this>\n");
for chunk in buffer.text_for_range(transform_range.clone()) {
document_content.push_str(chunk);
}
document_content.push_str("\n</rewrite_this>");
for chunk in buffer.text_for_range(truncated_after) {
document_content.push_str(chunk);
}
let rewrite_section = if !is_insert {
let mut section = String::new();
for chunk in buffer.text_for_range(range.clone()) {
section.push_str(chunk);
let mut rewrite_section = String::new();
for chunk in buffer.text_for_range(transform_range.clone()) {
rewrite_section.push_str(chunk);
}
let mut rewrite_section_prefix = String::new();
for chunk in buffer.text_for_range(transform_context_range.start..transform_range.start) {
rewrite_section_prefix.push_str(chunk);
}
let mut rewrite_section_suffix = String::new();
for chunk in buffer.text_for_range(transform_range.end..transform_context_range.end) {
rewrite_section_suffix.push_str(chunk);
}
let rewrite_section_with_edits = {
let mut section_with_selections = String::new();
let mut last_end = 0;
for selected_range in &selected_ranges {
if selected_range.start > last_end {
section_with_selections.push_str(
&rewrite_section[last_end..selected_range.start - transform_range.start],
);
}
if selected_range.start == selected_range.end {
section_with_selections.push_str("<insert_here></insert_here>");
} else {
section_with_selections.push_str("<edit_here>");
section_with_selections.push_str(
&rewrite_section[selected_range.start - transform_range.start
..selected_range.end - transform_range.start],
);
section_with_selections.push_str("</edit_here>");
}
last_end = selected_range.end - transform_range.start;
}
Some(section)
} else {
None
if last_end < rewrite_section.len() {
section_with_selections.push_str(&rewrite_section[last_end..]);
}
section_with_selections
};
let has_insertion = selected_ranges.iter().any(|range| range.start == range.end);
let has_replacement = selected_ranges.iter().any(|range| range.start != range.end);
let context = ContentPromptContext {
content_type: content_type.to_string(),
language_name: language_name.map(|s| s.to_string()),
is_insert,
is_truncated,
document_content,
user_prompt,
rewrite_section,
rewrite_section_prefix,
rewrite_section_suffix,
rewrite_section_with_edits,
has_insertion,
has_replacement,
};
self.handlebars.lock().render("content_prompt", &context)

View File

@@ -124,7 +124,6 @@ impl SlashCommandCompletionProvider {
&command_name,
&[],
true,
false,
workspace.clone(),
cx,
);
@@ -209,7 +208,6 @@ impl SlashCommandCompletionProvider {
&command_name,
&completed_arguments,
true,
false,
workspace.clone(),
cx,
);

View File

@@ -67,11 +67,7 @@ impl SlashCommand for ContextServerSlashCommand {
) -> Task<Result<SlashCommandOutput>> {
let server_id = self.server_id.clone();
let prompt_name = self.prompt.name.clone();
let prompt_args = match prompt_arguments(&self.prompt, arguments) {
Ok(args) => args,
Err(e) => return Task::ready(Err(e)),
};
let argument = arguments.first().cloned();
let manager = ContextServerManager::global(cx);
let manager = manager.read(cx);
@@ -80,7 +76,10 @@ impl SlashCommand for ContextServerSlashCommand {
let Some(protocol) = server.client.read().clone() else {
return Err(anyhow!("Context server not initialized"));
};
let result = protocol.run_prompt(&prompt_name, prompt_args).await?;
let result = protocol
.run_prompt(&prompt_name, prompt_arguments(&self.prompt, argument)?)
.await?;
Ok(SlashCommandOutput {
sections: vec![SlashCommandOutputSection {
@@ -98,27 +97,19 @@ impl SlashCommand for ContextServerSlashCommand {
}
}
fn prompt_arguments(prompt: &PromptInfo, arguments: &[String]) -> Result<HashMap<String, String>> {
fn prompt_arguments(
prompt: &PromptInfo,
argument: Option<String>,
) -> Result<HashMap<String, String>> {
match &prompt.arguments {
Some(args) if args.len() > 1 => Err(anyhow!(
Some(args) if args.len() >= 2 => Err(anyhow!(
"Prompt has more than one argument, which is not supported"
)),
Some(args) if args.len() == 1 => {
if !arguments.is_empty() {
let mut map = HashMap::default();
map.insert(args[0].name.clone(), arguments.join(" "));
Ok(map)
} else {
Err(anyhow!("Prompt expects argument but none given"))
}
}
Some(_) | None => {
if arguments.is_empty() {
Ok(HashMap::default())
} else {
Err(anyhow!("Prompt expects no arguments but some were given"))
}
}
Some(args) if args.len() == 1 => match argument {
Some(value) => Ok(HashMap::from_iter([(args[0].name.clone(), value)])),
None => Err(anyhow!("Prompt expects argument but none given")),
},
Some(_) | None => Ok(HashMap::default()),
}
}

View File

@@ -1,72 +1,64 @@
use std::sync::Arc;
use assistant_slash_command::SlashCommandRegistry;
use gpui::AnyElement;
use gpui::DismissEvent;
use gpui::WeakView;
use picker::PickerEditorPosition;
use std::sync::Arc;
use ui::ListItemSpacing;
use gpui::SharedString;
use gpui::Task;
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem, PopoverMenu, PopoverTrigger};
use ui::{prelude::*, ListItem, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
use crate::assistant_panel::ContextEditor;
#[derive(IntoElement)]
pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
pub struct SlashCommandSelector<T: PopoverTrigger> {
handle: Option<PopoverMenuHandle<Picker<SlashCommandDelegate>>>,
registry: Arc<SlashCommandRegistry>,
active_context_editor: WeakView<ContextEditor>,
trigger: T,
info_text: Option<SharedString>,
}
#[derive(Clone)]
struct SlashCommandInfo {
name: SharedString,
description: SharedString,
args: Option<SharedString>,
}
#[derive(Clone)]
enum SlashCommandEntry {
Info(SlashCommandInfo),
Advert {
name: SharedString,
renderer: fn(&mut WindowContext<'_>) -> AnyElement,
on_confirm: fn(&mut WindowContext<'_>),
},
}
impl AsRef<str> for SlashCommandEntry {
fn as_ref(&self) -> &str {
match self {
SlashCommandEntry::Info(SlashCommandInfo { name, .. })
| SlashCommandEntry::Advert { name, .. } => name,
}
}
}
pub(crate) struct SlashCommandDelegate {
all_commands: Vec<SlashCommandEntry>,
filtered_commands: Vec<SlashCommandEntry>,
pub struct SlashCommandDelegate {
all_commands: Vec<SlashCommandInfo>,
filtered_commands: Vec<SlashCommandInfo>,
active_context_editor: WeakView<ContextEditor>,
selected_index: usize,
}
impl<T: PopoverTrigger> SlashCommandSelector<T> {
pub(crate) fn new(
pub fn new(
registry: Arc<SlashCommandRegistry>,
active_context_editor: WeakView<ContextEditor>,
trigger: T,
) -> Self {
SlashCommandSelector {
handle: None,
registry,
active_context_editor,
trigger,
info_text: None,
}
}
pub fn with_handle(mut self, handle: PopoverMenuHandle<Picker<SlashCommandDelegate>>) -> Self {
self.handle = Some(handle);
self
}
pub fn with_info_text(mut self, text: impl Into<SharedString>) -> Self {
self.info_text = Some(text.into());
self
}
}
impl PickerDelegate for SlashCommandDelegate {
@@ -102,7 +94,7 @@ impl PickerDelegate for SlashCommandDelegate {
.into_iter()
.filter(|model_info| {
model_info
.as_ref()
.name
.to_lowercase()
.contains(&query.to_lowercase())
})
@@ -120,42 +112,13 @@ impl PickerDelegate for SlashCommandDelegate {
})
}
fn separators_after_indices(&self) -> Vec<usize> {
let mut ret = vec![];
let mut previous_is_advert = false;
for (index, command) in self.filtered_commands.iter().enumerate() {
if previous_is_advert {
if let SlashCommandEntry::Info(_) = command {
previous_is_advert = false;
debug_assert_ne!(
index, 0,
"index cannot be zero, as we can never have a separator at 0th position"
);
ret.push(index - 1);
}
} else {
if let SlashCommandEntry::Advert { .. } = command {
previous_is_advert = true;
if index != 0 {
ret.push(index - 1);
}
}
}
}
ret
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some(command) = self.filtered_commands.get(self.selected_index) {
if let SlashCommandEntry::Info(info) = command {
self.active_context_editor
.update(cx, |context_editor, cx| {
context_editor.insert_command(&info.name, cx)
})
.ok();
} else if let SlashCommandEntry::Advert { on_confirm, .. } = command {
on_confirm(cx);
}
self.active_context_editor
.update(cx, |context_editor, cx| {
context_editor.insert_command(&command.name, cx)
})
.ok();
cx.emit(DismissEvent);
}
}
@@ -170,63 +133,30 @@ impl PickerDelegate for SlashCommandDelegate {
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
_: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let command_info = self.filtered_commands.get(ix)?;
match command_info {
SlashCommandEntry::Info(info) => Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(
h_flex()
.group(format!("command-entry-label-{ix}"))
.w_full()
.min_w(px(220.))
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(
h_flex().w_full().min_w(px(220.)).child(
v_flex()
.child(
v_flex()
.child(
h_flex()
.child(div().font_buffer(cx).child({
let mut label = format!("/{}", info.name);
if let Some(args) =
info.args.as_ref().filter(|_| selected)
{
label.push_str(&args);
}
Label::new(label).size(LabelSize::Small)
}))
.children(info.args.clone().filter(|_| !selected).map(
|args| {
div()
.font_buffer(cx)
.child(
Label::new(args).size(LabelSize::Small),
)
.visible_on_hover(format!(
"command-entry-label-{ix}"
))
},
)),
)
.child(
Label::new(info.description.clone())
.size(LabelSize::Small)
.color(Color::Muted),
),
Label::new(format!("/{}", command_info.name))
.size(LabelSize::Small),
)
.child(
Label::new(command_info.description.clone())
.size(LabelSize::Small)
.color(Color::Muted),
),
),
),
SlashCommandEntry::Advert { renderer, .. } => Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(renderer(cx)),
),
}
),
)
}
}
@@ -239,41 +169,11 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
.filter_map(|command_name| {
let command = self.registry.command(&command_name)?;
let menu_text = SharedString::from(Arc::from(command.menu_text()));
let label = command.label(cx);
let args = label.filter_range.end.ne(&label.text.len()).then(|| {
SharedString::from(
label.text[label.filter_range.end..label.text.len()].to_owned(),
)
});
Some(SlashCommandEntry::Info(SlashCommandInfo {
Some(SlashCommandInfo {
name: command_name.into(),
description: menu_text,
args,
}))
})
})
.chain([SlashCommandEntry::Advert {
name: "create-your-command".into(),
renderer: |cx| {
v_flex()
.child(
h_flex()
.font_buffer(cx)
.items_center()
.gap_1()
.child(div().font_buffer(cx).child(
Label::new("create-your-command").size(LabelSize::Small),
))
.child(Icon::new(IconName::ArrowUpRight).size(IconSize::XSmall)),
)
.child(
Label::new("Learn how to create a custom command")
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element()
},
on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"),
}])
.collect::<Vec<_>>();
let delegate = SlashCommandDelegate {
@@ -288,10 +188,6 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
picker
});
let handle = self
.active_context_editor
.update(cx, |this, _| this.slash_menu_handle.clone())
.ok();
PopoverMenu::new("model-switcher")
.menu(move |_cx| Some(picker_view.clone()))
.trigger(self.trigger)
@@ -301,6 +197,5 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
x: px(0.0),
y: px(-16.0),
})
.when_some(handle, |this, handle| this.with_handle(handle))
}
}

View File

@@ -23,8 +23,6 @@ use workspace::Workspace;
pub use step_view::WorkflowStepView;
const IMPORTS_SYMBOL: &str = "#imports";
pub struct WorkflowStep {
context: WeakModel<Context>,
context_buffer_range: Range<Anchor>,
@@ -469,7 +467,7 @@ pub mod tool {
use super::*;
use anyhow::Context as _;
use gpui::AsyncAppContext;
use language::{Outline, OutlineItem, ParseStatus};
use language::ParseStatus;
use language_model::LanguageModelTool;
use project::ProjectPath;
use schemars::JsonSchema;
@@ -564,7 +562,10 @@ pub mod tool {
symbol,
description,
} => {
let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?;
let (symbol_path, symbol) = outline
.find_most_similar(&symbol)
.with_context(|| format!("symbol not found: {:?}", symbol))?;
let symbol = symbol.to_point(&snapshot);
let start = symbol
.annotation_range
.map_or(symbol.range.start, |range| range.start);
@@ -587,7 +588,10 @@ pub mod tool {
symbol,
description,
} => {
let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?;
let (symbol_path, symbol) = outline
.find_most_similar(&symbol)
.with_context(|| format!("symbol not found: {:?}", symbol))?;
let symbol = symbol.to_point(&snapshot);
let position = snapshot.anchor_before(
symbol
.annotation_range
@@ -605,7 +609,10 @@ pub mod tool {
symbol,
description,
} => {
let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?;
let (symbol_path, symbol) = outline
.find_most_similar(&symbol)
.with_context(|| format!("symbol not found: {:?}", symbol))?;
let symbol = symbol.to_point(&snapshot);
let position = snapshot.anchor_after(symbol.range.end);
WorkflowSuggestion::InsertSiblingAfter {
position,
@@ -618,8 +625,10 @@ pub mod tool {
description,
} => {
if let Some(symbol) = symbol {
let (symbol_path, symbol) =
Self::resolve_symbol(&snapshot, &outline, &symbol)?;
let (symbol_path, symbol) = outline
.find_most_similar(&symbol)
.with_context(|| format!("symbol not found: {:?}", symbol))?;
let symbol = symbol.to_point(&snapshot);
let position = snapshot.anchor_after(
symbol
@@ -644,8 +653,10 @@ pub mod tool {
description,
} => {
if let Some(symbol) = symbol {
let (symbol_path, symbol) =
Self::resolve_symbol(&snapshot, &outline, &symbol)?;
let (symbol_path, symbol) = outline
.find_most_similar(&symbol)
.with_context(|| format!("symbol not found: {:?}", symbol))?;
let symbol = symbol.to_point(&snapshot);
let position = snapshot.anchor_before(
symbol
@@ -666,7 +677,10 @@ pub mod tool {
}
}
WorkflowSuggestionToolKind::Delete { symbol } => {
let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?;
let (symbol_path, symbol) = outline
.find_most_similar(&symbol)
.with_context(|| format!("symbol not found: {:?}", symbol))?;
let symbol = symbol.to_point(&snapshot);
let start = symbol
.annotation_range
.map_or(symbol.range.start, |range| range.start);
@@ -682,60 +696,6 @@ pub mod tool {
Ok((buffer, suggestion))
}
fn resolve_symbol(
snapshot: &BufferSnapshot,
outline: &Outline<Anchor>,
symbol: &str,
) -> Result<(SymbolPath, OutlineItem<Point>)> {
if symbol == IMPORTS_SYMBOL {
let target_row = find_first_non_comment_line(snapshot);
Ok((
SymbolPath(IMPORTS_SYMBOL.to_string()),
OutlineItem {
range: Point::new(target_row, 0)..Point::new(target_row + 1, 0),
..Default::default()
},
))
} else {
let (symbol_path, symbol) = outline
.find_most_similar(symbol)
.with_context(|| format!("symbol not found: {symbol}"))?;
Ok((symbol_path, symbol.to_point(snapshot)))
}
}
}
fn find_first_non_comment_line(snapshot: &BufferSnapshot) -> u32 {
let Some(language) = snapshot.language() else {
return 0;
};
let scope = language.default_scope();
let comment_prefixes = scope.line_comment_prefixes();
let mut chunks = snapshot.as_rope().chunks();
let mut target_row = 0;
loop {
let starts_with_comment = chunks
.peek()
.map(|chunk| {
comment_prefixes
.iter()
.any(|s| chunk.starts_with(s.as_ref().trim_end()))
})
.unwrap_or(false);
if !starts_with_comment {
break;
}
target_row += 1;
if !chunks.next_line() {
break;
}
}
target_row
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]

View File

@@ -273,7 +273,7 @@ impl Item for WorkflowStepView {
}
fn tab_icon(&self, _cx: &WindowContext) -> Option<ui::Icon> {
Some(Icon::new(IconName::SearchCode))
Some(Icon::new(IconName::Pencil))
}
fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {

View File

@@ -27,6 +27,7 @@ fs.workspace = true
futures.workspace = true
gpui.workspace = true
http_client.workspace = true
lazy_static.workspace = true
log.workspace = true
once_cell.workspace = true
paths.workspace = true

View File

@@ -22,6 +22,7 @@ use gpui::{
actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, Task, WeakModel,
};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use lazy_static::lazy_static;
use parking_lot::RwLock;
use postage::watch;
use proto::ProtoClient;
@@ -42,7 +43,7 @@ use std::{
path::PathBuf,
sync::{
atomic::{AtomicU64, Ordering},
Arc, LazyLock, Weak,
Arc, Weak,
},
time::{Duration, Instant},
};
@@ -64,35 +65,27 @@ impl fmt::Display for DevServerToken {
}
}
static ZED_SERVER_URL: LazyLock<Option<String>> =
LazyLock::new(|| std::env::var("ZED_SERVER_URL").ok());
static ZED_RPC_URL: LazyLock<Option<String>> = LazyLock::new(|| std::env::var("ZED_RPC_URL").ok());
/// An environment variable whose presence indicates that the development auth
/// provider should be used.
///
/// Only works in development. Setting this environment variable in other release
/// channels is a no-op.
pub static ZED_DEVELOPMENT_AUTH: LazyLock<bool> = LazyLock::new(|| {
std::env::var("ZED_DEVELOPMENT_AUTH").map_or(false, |value| !value.is_empty())
});
pub static IMPERSONATE_LOGIN: LazyLock<Option<String>> = LazyLock::new(|| {
std::env::var("ZED_IMPERSONATE")
lazy_static! {
static ref ZED_SERVER_URL: Option<String> = std::env::var("ZED_SERVER_URL").ok();
static ref ZED_RPC_URL: Option<String> = std::env::var("ZED_RPC_URL").ok();
/// An environment variable whose presence indicates that the development auth
/// provider should be used.
///
/// Only works in development. Setting this environment variable in other release
/// channels is a no-op.
pub static ref ZED_DEVELOPMENT_AUTH: bool =
std::env::var("ZED_DEVELOPMENT_AUTH").map_or(false, |value| !value.is_empty());
pub static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
.ok()
.and_then(|s| if s.is_empty() { None } else { Some(s) })
});
pub static ADMIN_API_TOKEN: LazyLock<Option<String>> = LazyLock::new(|| {
std::env::var("ZED_ADMIN_API_TOKEN")
.and_then(|s| if s.is_empty() { None } else { Some(s) });
pub static ref ADMIN_API_TOKEN: Option<String> = std::env::var("ZED_ADMIN_API_TOKEN")
.ok()
.and_then(|s| if s.is_empty() { None } else { Some(s) })
});
pub static ZED_APP_PATH: LazyLock<Option<PathBuf>> =
LazyLock::new(|| std::env::var("ZED_APP_PATH").ok().map(PathBuf::from));
pub static ZED_ALWAYS_ACTIVE: LazyLock<bool> =
LazyLock::new(|| std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| !e.is_empty()));
.and_then(|s| if s.is_empty() { None } else { Some(s) });
pub static ref ZED_APP_PATH: Option<PathBuf> =
std::env::var("ZED_APP_PATH").ok().map(PathBuf::from);
pub static ref ZED_ALWAYS_ACTIVE: bool =
std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| !e.is_empty());
}
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(500);
pub const MAX_RECONNECTION_DELAY: Duration = Duration::from_secs(10);

View File

@@ -139,11 +139,6 @@ spec:
secretKeyRef:
name: anthropic
key: staff_api_key
- name: LLM_CLOSED_BETA_MODEL_NAME
valueFrom:
secretKeyRef:
name: llm-closed-beta
key: model_name
- name: GOOGLE_AI_API_KEY
valueFrom:
secretKeyRef:
@@ -216,12 +211,6 @@ spec:
secretKeyRef:
name: supermaven
key: api_key
- name: USER_BACKFILLER_GITHUB_ACCESS_TOKEN
valueFrom:
secretKeyRef:
name: user-backfiller
key: github_access_token
optional: true
- name: INVITE_LINK_PREFIX
value: ${INVITE_LINK_PREFIX}
- name: RUST_BACKTRACE

View File

@@ -1,4 +1,4 @@
db-uri = "postgres://postgres@localhost/zed"
server-port = 8081
db-uri = "postgres://postgres@localhost/zed_llm"
server-port = 8082
jwt-secret = "the-postgrest-jwt-secret-for-authorization"
log-level = "info"

View File

@@ -1,4 +1,4 @@
db-uri = "postgres://postgres@localhost/zed_llm"
server-port = 8082
db-uri = "postgres://postgres@localhost/zed"
server-port = 8081
jwt-secret = "the-postgrest-jwt-secret-for-authorization"
log-level = "info"

View File

@@ -377,14 +377,4 @@ impl Database {
})
.await
}
pub async fn get_users_missing_github_user_created_at(&self) -> Result<Vec<user::Model>> {
self.transaction(|tx| async move {
Ok(user::Entity::find()
.filter(user::Column::GithubUserCreatedAt.is_null())
.all(&*tx)
.await?)
})
.await
}
}

View File

@@ -9,7 +9,6 @@ pub mod migrations;
mod rate_limiter;
pub mod rpc;
pub mod seed;
pub mod user_backfiller;
#[cfg(test)]
mod tests;
@@ -169,7 +168,6 @@ pub struct Config {
pub google_ai_api_key: Option<Arc<str>>,
pub anthropic_api_key: Option<Arc<str>>,
pub anthropic_staff_api_key: Option<Arc<str>>,
pub llm_closed_beta_model_name: Option<Arc<str>>,
pub qwen2_7b_api_key: Option<Arc<str>>,
pub qwen2_7b_api_url: Option<Arc<str>>,
pub zed_client_checksum_seed: Option<String>,
@@ -178,7 +176,6 @@ pub struct Config {
pub stripe_api_key: Option<String>,
pub stripe_price_id: Option<Arc<str>>,
pub supermaven_admin_api_key: Option<Arc<str>>,
pub user_backfiller_github_access_token: Option<Arc<str>>,
}
impl Config {
@@ -222,7 +219,6 @@ impl Config {
google_ai_api_key: None,
anthropic_api_key: None,
anthropic_staff_api_key: None,
llm_closed_beta_model_name: None,
clickhouse_url: None,
clickhouse_user: None,
clickhouse_password: None,
@@ -237,7 +233,6 @@ impl Config {
supermaven_admin_api_key: None,
qwen2_7b_api_key: None,
qwen2_7b_api_url: None,
user_backfiller_github_access_token: None,
}
}
}

View File

@@ -141,8 +141,7 @@ async fn validate_api_token<B>(mut req: Request<B>, next: Next<B>) -> impl IntoR
tracing::Span::current()
.record("user_id", claims.user_id)
.record("login", claims.github_user_login.clone())
.record("authn.jti", &claims.jti)
.record("is_staff", &claims.is_staff);
.record("authn.jti", &claims.jti);
req.extensions_mut().insert(claims);
Ok::<_, Error>(next.run(req).await.into_response())
@@ -218,7 +217,7 @@ async fn perform_completion(
_ => request.model,
};
let (chunks, rate_limit_info) = anthropic::stream_completion_with_rate_limit_info(
let chunks = anthropic::stream_completion(
&state.http_client,
anthropic::ANTHROPIC_API_URL,
api_key,
@@ -246,19 +245,6 @@ async fn perform_completion(
anthropic::AnthropicError::Other(err) => Error::Internal(err),
})?;
if let Some(rate_limit_info) = rate_limit_info {
tracing::info!(
target: "upstream rate limit",
is_staff = claims.is_staff,
provider = params.provider.to_string(),
model = model,
tokens_remaining = rate_limit_info.tokens_remaining,
requests_remaining = rate_limit_info.requests_remaining,
requests_reset = ?rate_limit_info.requests_reset,
tokens_reset = ?rate_limit_info.tokens_reset,
);
}
chunks
.map(move |event| {
let chunk = event?;
@@ -554,75 +540,33 @@ impl<S> Drop for TokenCountingStream<S> {
.await
.log_err();
if let Some(usage) = usage {
tracing::info!(
target: "user usage",
user_id = claims.user_id,
login = claims.github_user_login,
authn.jti = claims.jti,
is_staff = claims.is_staff,
requests_this_minute = usage.requests_this_minute,
tokens_this_minute = usage.tokens_this_minute,
);
if let Some(clickhouse_client) = state.clickhouse_client.as_ref() {
report_llm_usage(
clickhouse_client,
LlmUsageEventRow {
time: Utc::now().timestamp_millis(),
user_id: claims.user_id as i32,
is_staff: claims.is_staff,
plan: match claims.plan {
Plan::Free => "free".to_string(),
Plan::ZedPro => "zed_pro".to_string(),
},
model,
provider: provider.to_string(),
input_token_count: input_token_count as u64,
output_token_count: output_token_count as u64,
requests_this_minute: usage.requests_this_minute as u64,
tokens_this_minute: usage.tokens_this_minute as u64,
tokens_this_day: usage.tokens_this_day as u64,
input_tokens_this_month: usage.input_tokens_this_month as u64,
output_tokens_this_month: usage.output_tokens_this_month as u64,
spending_this_month: usage.spending_this_month as u64,
lifetime_spending: usage.lifetime_spending as u64,
if let Some((clickhouse_client, usage)) = state.clickhouse_client.as_ref().zip(usage) {
report_llm_usage(
clickhouse_client,
LlmUsageEventRow {
time: Utc::now().timestamp_millis(),
user_id: claims.user_id as i32,
is_staff: claims.is_staff,
plan: match claims.plan {
Plan::Free => "free".to_string(),
Plan::ZedPro => "zed_pro".to_string(),
},
)
.await
.log_err();
}
model,
provider: provider.to_string(),
input_token_count: input_token_count as u64,
output_token_count: output_token_count as u64,
requests_this_minute: usage.requests_this_minute as u64,
tokens_this_minute: usage.tokens_this_minute as u64,
tokens_this_day: usage.tokens_this_day as u64,
input_tokens_this_month: usage.input_tokens_this_month as u64,
output_tokens_this_month: usage.output_tokens_this_month as u64,
spending_this_month: usage.spending_this_month as u64,
lifetime_spending: usage.lifetime_spending as u64,
},
)
.await
.log_err();
}
})
}
}
pub fn log_usage_periodically(state: Arc<LlmState>) {
state.executor.clone().spawn_detached(async move {
loop {
state
.executor
.sleep(std::time::Duration::from_secs(30))
.await;
let Some(usages) = state
.db
.get_application_wide_usages_by_model(Utc::now())
.await
.log_err()
else {
continue;
};
for usage in usages {
tracing::info!(
target: "computed usage",
provider = usage.provider.to_string(),
model = usage.model,
requests_this_minute = usage.requests_this_minute,
tokens_this_minute = usage.tokens_this_minute,
);
}
}
})
}

View File

@@ -12,12 +12,11 @@ pub fn authorize_access_to_language_model(
model: &str,
) -> Result<()> {
authorize_access_for_country(config, country_code, provider)?;
authorize_access_to_model(config, claims, provider, model)?;
authorize_access_to_model(claims, provider, model)?;
Ok(())
}
fn authorize_access_to_model(
config: &Config,
claims: &LlmTokenClaims,
provider: LanguageModelProvider,
model: &str,
@@ -26,25 +25,13 @@ fn authorize_access_to_model(
return Ok(());
}
match provider {
LanguageModelProvider::Anthropic => {
if model == "claude-3-5-sonnet" {
return Ok(());
}
if claims.has_llm_closed_beta_feature_flag
&& Some(model) == config.llm_closed_beta_model_name.as_deref()
{
return Ok(());
}
}
_ => {}
match (provider, model) {
(LanguageModelProvider::Anthropic, "claude-3-5-sonnet") => Ok(()),
_ => Err(Error::http(
StatusCode::FORBIDDEN,
format!("access to model {model:?} is not included in your plan"),
))?,
}
Err(Error::http(
StatusCode::FORBIDDEN,
format!("access to model {model:?} is not included in your plan"),
))
}
fn authorize_access_for_country(

View File

@@ -1,6 +1,5 @@
use crate::db::UserId;
use chrono::Duration;
use futures::StreamExt as _;
use rpc::LanguageModelProvider;
use sea_orm::QuerySelect;
use std::{iter, str::FromStr};
@@ -19,14 +18,6 @@ pub struct Usage {
pub lifetime_spending: usize,
}
#[derive(Debug, PartialEq, Clone)]
pub struct ApplicationWideUsage {
pub provider: LanguageModelProvider,
pub model: String,
pub requests_this_minute: usize,
pub tokens_this_minute: usize,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct ActiveUserCount {
pub users_in_recent_minutes: usize,
@@ -72,72 +63,6 @@ impl LlmDatabase {
Ok(())
}
pub async fn get_application_wide_usages_by_model(
&self,
now: DateTimeUtc,
) -> Result<Vec<ApplicationWideUsage>> {
self.transaction(|tx| async move {
let past_minute = now - Duration::minutes(1);
let requests_per_minute = self.usage_measure_ids[&UsageMeasure::RequestsPerMinute];
let tokens_per_minute = self.usage_measure_ids[&UsageMeasure::TokensPerMinute];
let mut results = Vec::new();
for ((provider, model_name), model) in self.models.iter() {
let mut usages = usage::Entity::find()
.filter(
usage::Column::Timestamp
.gte(past_minute.naive_utc())
.and(usage::Column::IsStaff.eq(false))
.and(usage::Column::ModelId.eq(model.id))
.and(
usage::Column::MeasureId
.eq(requests_per_minute)
.or(usage::Column::MeasureId.eq(tokens_per_minute)),
),
)
.stream(&*tx)
.await?;
let mut requests_this_minute = 0;
let mut tokens_this_minute = 0;
while let Some(usage) = usages.next().await {
let usage = usage?;
if usage.measure_id == requests_per_minute {
requests_this_minute += Self::get_live_buckets(
&usage,
now.naive_utc(),
UsageMeasure::RequestsPerMinute,
)
.0
.iter()
.copied()
.sum::<i64>() as usize;
} else if usage.measure_id == tokens_per_minute {
tokens_this_minute += Self::get_live_buckets(
&usage,
now.naive_utc(),
UsageMeasure::TokensPerMinute,
)
.0
.iter()
.copied()
.sum::<i64>() as usize;
}
}
results.push(ApplicationWideUsage {
provider: *provider,
model: model_name.clone(),
requests_this_minute,
tokens_this_minute,
})
}
Ok(results)
})
.await
}
pub async fn get_usage(
&self,
user_id: UserId,

View File

@@ -20,8 +20,6 @@ pub struct LlmTokenClaims {
#[serde(default)]
pub github_user_login: Option<String>,
pub is_staff: bool,
#[serde(default)]
pub has_llm_closed_beta_feature_flag: bool,
pub plan: rpc::proto::Plan,
}
@@ -32,7 +30,6 @@ impl LlmTokenClaims {
user_id: UserId,
github_user_login: String,
is_staff: bool,
has_llm_closed_beta_feature_flag: bool,
plan: rpc::proto::Plan,
config: &Config,
) -> Result<String> {
@@ -49,7 +46,6 @@ impl LlmTokenClaims {
user_id: user_id.to_proto(),
github_user_login: Some(github_user_login),
is_staff,
has_llm_closed_beta_feature_flag,
plan,
};

View File

@@ -5,9 +5,8 @@ use axum::{
routing::get,
Extension, Router,
};
use collab::llm::{db::LlmDatabase, log_usage_periodically};
use collab::llm::db::LlmDatabase;
use collab::migrations::run_database_migrations;
use collab::user_backfiller::spawn_user_backfiller;
use collab::{api::billing::poll_stripe_events_periodically, llm::LlmState, ServiceMode};
use collab::{
api::fetch_extensions_from_blob_store_periodically, db, env, executor::Executor,
@@ -96,8 +95,6 @@ async fn main() -> Result<()> {
let state = LlmState::new(config.clone(), Executor::Production).await?;
log_usage_periodically(state.clone());
app = app
.merge(collab::llm::routes())
.layer(Extension(state.clone()));
@@ -132,7 +129,6 @@ async fn main() -> Result<()> {
if mode.is_api() {
poll_stripe_events_periodically(state.clone());
fetch_extensions_from_blob_store_periodically(state.clone());
spawn_user_backfiller(state.clone());
app = app
.merge(collab::api::events::router())
@@ -156,8 +152,7 @@ async fn main() -> Result<()> {
matched_path,
user_id = tracing::field::Empty,
login = tracing::field::Empty,
authn.jti = tracing::field::Empty,
is_staff = tracing::field::Empty
authn.jti = tracing::field::Empty
)
})
.on_response(

View File

@@ -4918,10 +4918,7 @@ async fn get_llm_api_token(
let db = session.db().await;
let flags = db.get_user_flags(session.user_id()).await?;
let has_language_models_feature_flag = flags.iter().any(|flag| flag == "language-models");
let has_llm_closed_beta_feature_flag = flags.iter().any(|flag| flag == "llm-closed-beta");
if !session.is_staff() && !has_language_models_feature_flag {
if !session.is_staff() && !flags.iter().any(|flag| flag == "language-models") {
Err(anyhow!("permission denied"))?
}
@@ -4946,7 +4943,6 @@ async fn get_llm_api_token(
user.id,
user.github_login.clone(),
session.is_staff(),
has_llm_closed_beta_feature_flag,
session.current_plan(db).await?,
&session.app_state.config,
)?;

View File

@@ -1,6 +1,7 @@
use crate::db::{self, ChannelRole, NewUserParams};
use anyhow::Context;
use chrono::{DateTime, Utc};
use db::Database;
use serde::{de::DeserializeOwned, Deserialize};
use std::{fmt::Write, fs, path::Path};
@@ -12,6 +13,7 @@ struct GitHubUser {
id: i32,
login: String,
email: Option<String>,
created_at: DateTime<Utc>,
}
#[derive(Deserialize)]
@@ -42,17 +44,6 @@ pub async fn seed(config: &Config, db: &Database, force: bool) -> anyhow::Result
let mut first_user = None;
let mut others = vec![];
let flag_names = ["remoting", "language-models"];
let mut flags = Vec::new();
for flag_name in flag_names {
let flag = db
.create_user_flag(flag_name, false)
.await
.unwrap_or_else(|_| panic!("failed to create flag: '{flag_name}'"));
flags.push(flag);
}
for admin_login in seed_config.admins {
let user = fetch_github::<GitHubUser>(
&client,
@@ -75,15 +66,6 @@ pub async fn seed(config: &Config, db: &Database, force: bool) -> anyhow::Result
} else {
others.push(user.user_id)
}
for flag in &flags {
db.add_user_flag(user.user_id, *flag)
.await
.context(format!(
"Unable to enable flag '{}' for user '{}'",
flag, user.user_id
))?;
}
}
for channel in seed_config.channels {
@@ -104,7 +86,6 @@ pub async fn seed(config: &Config, db: &Database, force: bool) -> anyhow::Result
}
}
// TODO: Fix this later
if let Some(number_of_users) = seed_config.number_of_users {
// Fetch 100 other random users from GitHub and insert them into the database
// (for testing autocompleters, etc.)
@@ -124,23 +105,15 @@ pub async fn seed(config: &Config, db: &Database, force: bool) -> anyhow::Result
for github_user in users {
last_user_id = Some(github_user.id);
user_count += 1;
let user = db
.get_or_create_user_by_github_account(
&github_user.login,
Some(github_user.id),
github_user.email.as_deref(),
None,
None,
)
.await
.expect("failed to insert user");
for flag in &flags {
db.add_user_flag(user.id, *flag).await.context(format!(
"Unable to enable flag '{}' for user '{}'",
flag, user.id
))?;
}
db.get_or_create_user_by_github_account(
&github_user.login,
Some(github_user.id),
github_user.email.as_deref(),
Some(github_user.created_at),
None,
)
.await
.expect("failed to insert user");
}
}
}
@@ -159,9 +132,9 @@ async fn fetch_github<T: DeserializeOwned>(client: &reqwest::Client, url: &str)
.header("user-agent", "zed")
.send()
.await
.unwrap_or_else(|error| panic!("failed to fetch '{url}': {error}"));
.unwrap_or_else(|_| panic!("failed to fetch '{}'", url));
response
.json()
.await
.unwrap_or_else(|error| panic!("failed to deserialize github user from '{url}': {error}"))
.unwrap_or_else(|_| panic!("failed to deserialize github user from '{}'", url))
}

View File

@@ -667,7 +667,6 @@ impl TestServer {
google_ai_api_key: None,
anthropic_api_key: None,
anthropic_staff_api_key: None,
llm_closed_beta_model_name: None,
clickhouse_url: None,
clickhouse_user: None,
clickhouse_password: None,
@@ -682,7 +681,6 @@ impl TestServer {
supermaven_admin_api_key: None,
qwen2_7b_api_key: None,
qwen2_7b_api_url: None,
user_backfiller_github_access_token: None,
},
})
}

View File

@@ -1,162 +0,0 @@
use std::sync::Arc;
use anyhow::{anyhow, Context, Result};
use chrono::{DateTime, Utc};
use util::ResultExt;
use crate::db::Database;
use crate::executor::Executor;
use crate::{AppState, Config};
pub fn spawn_user_backfiller(app_state: Arc<AppState>) {
let Some(user_backfiller_github_access_token) =
app_state.config.user_backfiller_github_access_token.clone()
else {
log::info!("no USER_BACKFILLER_GITHUB_ACCESS_TOKEN set; not spawning user backfiller");
return;
};
let executor = app_state.executor.clone();
executor.spawn_detached({
let executor = executor.clone();
async move {
let user_backfiller = UserBackfiller::new(
app_state.config.clone(),
user_backfiller_github_access_token,
app_state.db.clone(),
executor,
);
log::info!("backfilling users");
user_backfiller
.backfill_github_user_created_at()
.await
.log_err();
}
});
}
const GITHUB_REQUESTS_PER_HOUR_LIMIT: usize = 5_000;
const SLEEP_DURATION_BETWEEN_USERS: std::time::Duration = std::time::Duration::from_millis(
(GITHUB_REQUESTS_PER_HOUR_LIMIT as f64 / 60. / 60. * 1000.) as u64,
);
struct UserBackfiller {
config: Config,
github_access_token: Arc<str>,
db: Arc<Database>,
http_client: reqwest::Client,
executor: Executor,
}
impl UserBackfiller {
fn new(
config: Config,
github_access_token: Arc<str>,
db: Arc<Database>,
executor: Executor,
) -> Self {
Self {
config,
github_access_token,
db,
http_client: reqwest::Client::new(),
executor,
}
}
async fn backfill_github_user_created_at(&self) -> Result<()> {
let initial_channel_id = self.config.auto_join_channel_id;
let users_missing_github_user_created_at =
self.db.get_users_missing_github_user_created_at().await?;
for user in users_missing_github_user_created_at {
match self
.fetch_github_user(&format!(
"https://api.github.com/users/{}",
user.github_login
))
.await
{
Ok(github_user) => {
self.db
.get_or_create_user_by_github_account(
&user.github_login,
Some(github_user.id),
user.email_address.as_deref(),
Some(github_user.created_at),
initial_channel_id,
)
.await?;
log::info!("backfilled user: {}", user.github_login);
}
Err(err) => {
log::error!("failed to fetch GitHub user {}: {err}", user.github_login);
}
}
self.executor.sleep(SLEEP_DURATION_BETWEEN_USERS).await;
}
Ok(())
}
async fn fetch_github_user(&self, url: &str) -> Result<GithubUser> {
let response = self
.http_client
.get(url)
.header(
"authorization",
format!("Bearer {}", self.github_access_token),
)
.header("user-agent", "zed")
.send()
.await
.with_context(|| format!("failed to fetch '{url}'"))?;
let rate_limit_remaining = response
.headers()
.get("x-ratelimit-remaining")
.and_then(|value| value.to_str().ok())
.and_then(|value| value.parse::<i32>().ok());
let rate_limit_reset = response
.headers()
.get("x-ratelimit-reset")
.and_then(|value| value.to_str().ok())
.and_then(|value| value.parse::<i64>().ok())
.and_then(|value| DateTime::from_timestamp(value, 0));
if rate_limit_remaining == Some(0) {
if let Some(reset_at) = rate_limit_reset {
let now = Utc::now();
if reset_at > now {
let sleep_duration = reset_at - now;
log::info!(
"rate limit reached. Sleeping for {} seconds",
sleep_duration.num_seconds()
);
self.executor.sleep(sleep_duration.to_std().unwrap()).await;
}
}
}
let response = match response.error_for_status() {
Ok(response) => response,
Err(err) => return Err(anyhow!("failed to fetch GitHub user: {err}")),
};
response
.json()
.await
.with_context(|| format!("failed to deserialize GitHub user from '{url}'"))
}
}
#[derive(serde::Deserialize)]
struct GithubUser {
id: i32,
created_at: DateTime<Utc>,
}

View File

@@ -42,6 +42,7 @@ futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
language.workspace = true
lazy_static.workspace = true
menu.workspace = true
notifications.workspace = true
parking_lot.workspace = true

View File

@@ -12,10 +12,11 @@ use language::{
language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry,
LanguageServerId, ToOffset,
};
use lazy_static::lazy_static;
use parking_lot::RwLock;
use project::{search::SearchQuery, Completion};
use settings::Settings;
use std::{ops::Range, sync::Arc, sync::LazyLock, time::Duration};
use std::{ops::Range, sync::Arc, time::Duration};
use theme::ThemeSettings;
use ui::{prelude::*, TextSize};
@@ -23,17 +24,17 @@ use crate::panel_settings::MessageEditorSettings;
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
static MENTIONS_SEARCH: LazyLock<SearchQuery> = LazyLock::new(|| {
SearchQuery::regex(
lazy_static! {
static ref MENTIONS_SEARCH: SearchQuery = SearchQuery::regex(
"@[-_\\w]+",
false,
false,
false,
Default::default(),
Default::default(),
Default::default()
)
.unwrap()
});
.unwrap();
}
pub struct MessageEditor {
pub editor: View<Editor>,
@@ -398,8 +399,8 @@ impl MessageEditor {
end_anchor: Anchor,
cx: &mut ViewContext<Self>,
) -> Option<(Anchor, String, &'static [StringMatchCandidate])> {
static EMOJI_FUZZY_MATCH_CANDIDATES: LazyLock<Vec<StringMatchCandidate>> =
LazyLock::new(|| {
lazy_static! {
static ref EMOJI_FUZZY_MATCH_CANDIDATES: Vec<StringMatchCandidate> = {
let emojis = emojis::iter()
.flat_map(|s| s.shortcodes())
.map(|emoji| StringMatchCandidate {
@@ -409,7 +410,8 @@ impl MessageEditor {
})
.collect::<Vec<_>>();
emojis
});
};
}
let end_offset = end_anchor.to_offset(buffer.read(cx));

View File

@@ -1395,22 +1395,15 @@ impl CollabPanel {
cx.notify();
}
fn reset_filter_editor_text(&mut self, cx: &mut ViewContext<Self>) -> bool {
self.filter_editor.update(cx, |editor, cx| {
if editor.buffer().read(cx).len(cx) > 0 {
editor.set_text("", cx);
true
} else {
false
}
})
}
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
if self.take_editing_state(cx) {
cx.focus_view(&self.filter_editor);
} else if !self.reset_filter_editor_text(cx) {
self.focus_handle.focus(cx);
} else {
self.filter_editor.update(cx, |editor, cx| {
if editor.buffer().read(cx).len(cx) > 0 {
editor.set_text("", cx);
}
});
}
if self.context_menu.is_some() {

View File

@@ -30,9 +30,7 @@ fn restart_servers(_workspace: &mut Workspace, _action: &Restart, cx: &mut ViewC
let model = ContextServerManager::global(&cx);
cx.update_model(&model, |manager, cx| {
for server in manager.servers() {
manager
.restart_server(&server.id, cx)
.detach_and_log_err(cx);
manager.restart_server(&server.id, cx).detach();
}
});
}

View File

@@ -266,11 +266,11 @@ pub fn init(cx: &mut AppContext) {
log::trace!("servers_to_add={:?}", servers_to_add);
for config in servers_to_add {
manager.add_server(config, cx).detach_and_log_err(cx);
manager.add_server(config, cx).detach();
}
for id in servers_to_remove {
manager.remove_server(&id, cx).detach_and_log_err(cx);
manager.remove_server(&id, cx).detach();
}
})
})

View File

@@ -31,8 +31,6 @@ pub enum Role {
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
pub enum Model {
#[default]
#[serde(alias = "gpt-4o", rename = "gpt-4o-2024-05-13")]
Gpt4o,
#[serde(alias = "gpt-4", rename = "gpt-4")]
Gpt4,
#[serde(alias = "gpt-3.5-turbo", rename = "gpt-3.5-turbo")]
@@ -42,7 +40,6 @@ pub enum Model {
impl Model {
pub fn from_id(id: &str) -> Result<Self> {
match id {
"gpt-4o" => Ok(Self::Gpt4o),
"gpt-4" => Ok(Self::Gpt4),
"gpt-3.5-turbo" => Ok(Self::Gpt3_5Turbo),
_ => Err(anyhow!("Invalid model id: {}", id)),
@@ -53,7 +50,6 @@ impl Model {
match self {
Self::Gpt3_5Turbo => "gpt-3.5-turbo",
Self::Gpt4 => "gpt-4",
Self::Gpt4o => "gpt-4o",
}
}
@@ -61,13 +57,11 @@ impl Model {
match self {
Self::Gpt3_5Turbo => "GPT-3.5",
Self::Gpt4 => "GPT-4",
Self::Gpt4o => "GPT-4o",
}
}
pub fn max_token_count(&self) -> usize {
match self {
Self::Gpt4o => 128000,
Self::Gpt4 => 8192,
Self::Gpt3_5Turbo => 16385,
}

View File

@@ -19,6 +19,7 @@ test-support = []
anyhow.workspace = true
gpui.workspace = true
indoc.workspace = true
lazy_static.workspace = true
log.workspace = true
paths.workspace = true
release_channel.workspace = true

View File

@@ -6,6 +6,7 @@ pub use anyhow;
use anyhow::Context;
use gpui::AppContext;
pub use indoc::indoc;
pub use lazy_static;
pub use paths::database_dir;
pub use smol;
pub use sqlez;
@@ -16,11 +17,9 @@ pub use release_channel::RELEASE_CHANNEL;
use sqlez::domain::Migrator;
use sqlez::thread_safe_connection::ThreadSafeConnection;
use sqlez_macros::sql;
use std::env;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::LazyLock;
use util::{maybe, ResultExt};
const CONNECTION_INITIALIZE_QUERY: &str = sql!(
@@ -38,10 +37,10 @@ const FALLBACK_DB_NAME: &str = "FALLBACK_MEMORY_DB";
const DB_FILE_NAME: &str = "db.sqlite";
pub static ZED_STATELESS: LazyLock<bool> =
LazyLock::new(|| env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()));
pub static ALL_FILE_DB_FAILED: LazyLock<AtomicBool> = LazyLock::new(|| AtomicBool::new(false));
lazy_static::lazy_static! {
pub static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false);
}
/// Open or create a database at the given directory path.
/// This will retry a couple times if there are failures. If opening fails once, the db directory
@@ -139,16 +138,15 @@ macro_rules! define_connection {
}
}
use std::sync::LazyLock;
#[cfg(any(test, feature = "test-support"))]
pub static $id: LazyLock<$t> = LazyLock::new(|| {
$t($crate::smol::block_on($crate::open_test_db(stringify!($id))))
});
$crate::lazy_static::lazy_static! {
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_test_db(stringify!($id))));
}
#[cfg(not(any(test, feature = "test-support")))]
pub static $id: LazyLock<$t> = LazyLock::new(|| {
$t($crate::smol::block_on($crate::open_db($crate::database_dir(), &$crate::RELEASE_CHANNEL)))
});
$crate::lazy_static::lazy_static! {
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_db($crate::database_dir(), &$crate::RELEASE_CHANNEL)));
}
};
(pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr;) => {
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection<( $($d),+, $t )>);
@@ -172,14 +170,14 @@ macro_rules! define_connection {
}
#[cfg(any(test, feature = "test-support"))]
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
$t($crate::smol::block_on($crate::open_test_db(stringify!($id))))
});
$crate::lazy_static::lazy_static! {
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_test_db(stringify!($id))));
}
#[cfg(not(any(test, feature = "test-support")))]
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
$t($crate::smol::block_on($crate::open_db($crate::database_dir(), &$crate::RELEASE_CHANNEL)))
});
$crate::lazy_static::lazy_static! {
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_db($crate::database_dir(), &$crate::RELEASE_CHANNEL)));
}
};
}

View File

@@ -47,6 +47,7 @@ http_client.workspace = true
indoc.workspace = true
itertools.workspace = true
language.workspace = true
lazy_static.workspace = true
linkify.workspace = true
log.workspace = true
lsp.workspace = true

View File

@@ -74,6 +74,8 @@ pub enum FoldStatus {
pub type RenderFoldToggle = Arc<dyn Fn(FoldStatus, &mut WindowContext) -> AnyElement>;
const UNNECESSARY_CODE_FADE: f32 = 0.3;
pub trait ToDisplayPoint {
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
}
@@ -689,7 +691,7 @@ impl DisplaySnapshot {
let mut diagnostic_highlight = HighlightStyle::default();
if chunk.is_unnecessary {
diagnostic_highlight.fade_out = Some(editor_style.unnecessary_code_fade);
diagnostic_highlight.fade_out = Some(UNNECESSARY_CODE_FADE);
}
if let Some(severity) = chunk.diagnostic_severity {

View File

@@ -5,9 +5,9 @@ use super::{
};
use gpui::{AppContext, Context, Font, LineWrapper, Model, ModelContext, Pixels, Task};
use language::{Chunk, Point};
use lazy_static::lazy_static;
use multi_buffer::MultiBufferSnapshot;
use smol::future::yield_now;
use std::sync::LazyLock;
use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration};
use sum_tree::{Bias, Cursor, SumTree};
use text::Patch;
@@ -887,12 +887,14 @@ impl Transform {
}
fn wrap(indent: u32) -> Self {
static WRAP_TEXT: LazyLock<String> = LazyLock::new(|| {
let mut wrap_text = String::new();
wrap_text.push('\n');
wrap_text.extend((0..LineWrapper::MAX_INDENT as usize).map(|_| ' '));
wrap_text
});
lazy_static! {
static ref WRAP_TEXT: String = {
let mut wrap_text = String::new();
wrap_text.push('\n');
wrap_text.extend((0..LineWrapper::MAX_INDENT as usize).map(|_| ' '));
wrap_text
};
}
Self {
summary: TransformSummary {

View File

@@ -266,22 +266,6 @@ pub enum Direction {
Next,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Navigated {
Yes,
No,
}
impl Navigated {
pub fn from_bool(yes: bool) -> Navigated {
if yes {
Navigated::Yes
} else {
Navigated::No
}
}
}
pub fn init_settings(cx: &mut AppContext) {
EditorSettings::register(cx);
}
@@ -384,7 +368,6 @@ pub struct EditorStyle {
pub status: StatusColors,
pub inlay_hints_style: HighlightStyle,
pub suggestions_style: HighlightStyle,
pub unnecessary_code_fade: f32,
}
impl Default for EditorStyle {
@@ -401,7 +384,6 @@ impl Default for EditorStyle {
status: StatusColors::dark(),
inlay_hints_style: HighlightStyle::default(),
suggestions_style: HighlightStyle::default(),
unnecessary_code_fade: Default::default(),
}
}
}
@@ -462,14 +444,6 @@ struct ResolvedTasks {
struct MultiBufferOffset(usize);
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
struct BufferOffset(usize);
// Addons allow storing per-editor state in other crates (e.g. Vim)
pub trait Addon: 'static {
fn extend_key_context(&self, _: &mut KeyContext, _: &AppContext) {}
fn to_any(&self) -> &dyn std::any::Any;
}
/// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`]
///
/// See the [module level documentation](self) for more information.
@@ -541,6 +515,7 @@ pub struct Editor {
collapse_matches: bool,
autoindent_mode: Option<AutoindentMode>,
workspace: Option<(WeakView<Workspace>, Option<WorkspaceId>)>,
keymap_context_layers: BTreeMap<TypeId, KeyContext>,
input_enabled: bool,
use_modal_editing: bool,
read_only: bool,
@@ -558,6 +533,7 @@ pub struct Editor {
_subscriptions: Vec<Subscription>,
pixel_position_of_newest_cursor: Option<gpui::Point<Pixels>>,
gutter_dimensions: GutterDimensions,
pub vim_replace_map: HashMap<Range<usize>, String>,
style: Option<EditorStyle>,
next_editor_action_id: EditorActionId,
editor_actions: Rc<RefCell<BTreeMap<EditorActionId, Box<dyn Fn(&mut ViewContext<Self>)>>>>,
@@ -587,7 +563,6 @@ pub struct Editor {
breadcrumb_header: Option<String>,
focused_block: Option<FocusedBlock>,
next_scroll_position: NextScrollCursorCenterTopBottom,
addons: HashMap<TypeId, Box<dyn Addon>>,
_scroll_cursor_center_top_bottom_task: Task<()>,
}
@@ -1586,7 +1561,6 @@ pub(crate) struct NavigationData {
scroll_top_row: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GotoDefinitionKind {
Symbol,
Declaration,
@@ -1882,6 +1856,7 @@ impl Editor {
autoindent_mode: Some(AutoindentMode::EachLine),
collapse_matches: false,
workspace: None,
keymap_context_layers: Default::default(),
input_enabled: true,
use_modal_editing: mode == EditorMode::Full,
read_only: false,
@@ -1906,6 +1881,7 @@ impl Editor {
hovered_cursors: Default::default(),
next_editor_action_id: EditorActionId::default(),
editor_actions: Rc::default(),
vim_replace_map: Default::default(),
show_inline_completions: mode == EditorMode::Full,
custom_context_menu: None,
show_git_blame_gutter: false,
@@ -1944,7 +1920,6 @@ impl Editor {
breadcrumb_header: None,
focused_block: None,
next_scroll_position: NextScrollCursorCenterTopBottom::default(),
addons: HashMap::default(),
_scroll_cursor_center_top_bottom_task: Task::ready(()),
};
this.tasks_update_task = Some(this.refresh_runnables(cx));
@@ -1967,13 +1942,13 @@ impl Editor {
this
}
pub fn mouse_menu_is_focused(&self, cx: &WindowContext) -> bool {
pub fn mouse_menu_is_focused(&self, cx: &mut WindowContext) -> bool {
self.mouse_context_menu
.as_ref()
.is_some_and(|menu| menu.context_menu.focus_handle(cx).is_focused(cx))
}
fn key_context(&self, cx: &ViewContext<Self>) -> KeyContext {
fn key_context(&self, cx: &AppContext) -> KeyContext {
let mut key_context = KeyContext::new_with_defaults();
key_context.add("Editor");
let mode = match self.mode {
@@ -2004,13 +1979,8 @@ impl Editor {
}
}
// Disable vim contexts when a sub-editor (e.g. rename/inline assistant) is focused.
if !self.focus_handle(cx).contains_focused(cx)
|| (self.is_focused(cx) || self.mouse_menu_is_focused(cx))
{
for addon in self.addons.values() {
addon.extend_key_context(&mut key_context, cx)
}
for layer in self.keymap_context_layers.values() {
key_context.extend(layer);
}
if let Some(extension) = self
@@ -2252,6 +2222,21 @@ impl Editor {
}
}
pub fn set_keymap_context_layer<Tag: 'static>(
&mut self,
context: KeyContext,
cx: &mut ViewContext<Self>,
) {
self.keymap_context_layers
.insert(TypeId::of::<Tag>(), context);
cx.notify();
}
pub fn remove_keymap_context_layer<Tag: 'static>(&mut self, cx: &mut ViewContext<Self>) {
self.keymap_context_layers.remove(&TypeId::of::<Tag>());
cx.notify();
}
pub fn set_input_enabled(&mut self, input_enabled: bool) {
self.input_enabled = input_enabled;
}
@@ -4858,7 +4843,7 @@ impl Editor {
let range = Anchor {
buffer_id,
excerpt_id,
excerpt_id: excerpt_id,
text_anchor: start,
}..Anchor {
buffer_id,
@@ -9035,28 +9020,15 @@ impl Editor {
&mut self,
_: &GoToDefinition,
cx: &mut ViewContext<Self>,
) -> Task<Result<Navigated>> {
let definition = self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, cx);
let references = self.find_all_references(&FindAllReferences, cx);
cx.background_executor().spawn(async move {
if definition.await? == Navigated::Yes {
return Ok(Navigated::Yes);
}
if let Some(references) = references {
if references.await? == Navigated::Yes {
return Ok(Navigated::Yes);
}
}
Ok(Navigated::No)
})
) -> Task<Result<bool>> {
self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, cx)
}
pub fn go_to_declaration(
&mut self,
_: &GoToDeclaration,
cx: &mut ViewContext<Self>,
) -> Task<Result<Navigated>> {
) -> Task<Result<bool>> {
self.go_to_definition_of_kind(GotoDefinitionKind::Declaration, false, cx)
}
@@ -9064,7 +9036,7 @@ impl Editor {
&mut self,
_: &GoToDeclaration,
cx: &mut ViewContext<Self>,
) -> Task<Result<Navigated>> {
) -> Task<Result<bool>> {
self.go_to_definition_of_kind(GotoDefinitionKind::Declaration, true, cx)
}
@@ -9072,7 +9044,7 @@ impl Editor {
&mut self,
_: &GoToImplementation,
cx: &mut ViewContext<Self>,
) -> Task<Result<Navigated>> {
) -> Task<Result<bool>> {
self.go_to_definition_of_kind(GotoDefinitionKind::Implementation, false, cx)
}
@@ -9080,7 +9052,7 @@ impl Editor {
&mut self,
_: &GoToImplementationSplit,
cx: &mut ViewContext<Self>,
) -> Task<Result<Navigated>> {
) -> Task<Result<bool>> {
self.go_to_definition_of_kind(GotoDefinitionKind::Implementation, true, cx)
}
@@ -9088,7 +9060,7 @@ impl Editor {
&mut self,
_: &GoToTypeDefinition,
cx: &mut ViewContext<Self>,
) -> Task<Result<Navigated>> {
) -> Task<Result<bool>> {
self.go_to_definition_of_kind(GotoDefinitionKind::Type, false, cx)
}
@@ -9096,7 +9068,7 @@ impl Editor {
&mut self,
_: &GoToDefinitionSplit,
cx: &mut ViewContext<Self>,
) -> Task<Result<Navigated>> {
) -> Task<Result<bool>> {
self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, true, cx)
}
@@ -9104,7 +9076,7 @@ impl Editor {
&mut self,
_: &GoToTypeDefinitionSplit,
cx: &mut ViewContext<Self>,
) -> Task<Result<Navigated>> {
) -> Task<Result<bool>> {
self.go_to_definition_of_kind(GotoDefinitionKind::Type, true, cx)
}
@@ -9113,16 +9085,16 @@ impl Editor {
kind: GotoDefinitionKind,
split: bool,
cx: &mut ViewContext<Self>,
) -> Task<Result<Navigated>> {
) -> Task<Result<bool>> {
let Some(workspace) = self.workspace() else {
return Task::ready(Ok(Navigated::No));
return Task::ready(Ok(false));
};
let buffer = self.buffer.read(cx);
let head = self.selections.newest::<usize>(cx).head();
let (buffer, head) = if let Some(text_anchor) = buffer.text_anchor_for_position(head, cx) {
text_anchor
} else {
return Task::ready(Ok(Navigated::No));
return Task::ready(Ok(false));
};
let project = workspace.read(cx).project().clone();
@@ -9181,7 +9153,7 @@ impl Editor {
mut definitions: Vec<HoverLink>,
split: bool,
cx: &mut ViewContext<Editor>,
) -> Task<Result<Navigated>> {
) -> Task<Result<bool>> {
// If there is one definition, just open it directly
if definitions.len() == 1 {
let definition = definitions.pop().unwrap();
@@ -9197,61 +9169,77 @@ impl Editor {
};
cx.spawn(|editor, mut cx| async move {
let target = target_task.await.context("target resolution task")?;
let Some(target) = target else {
return Ok(Navigated::No);
};
editor.update(&mut cx, |editor, cx| {
let Some(workspace) = editor.workspace() else {
return Navigated::No;
};
let pane = workspace.read(cx).active_pane().clone();
if let Some(target) = target {
editor.update(&mut cx, |editor, cx| {
let Some(workspace) = editor.workspace() else {
return false;
};
let pane = workspace.read(cx).active_pane().clone();
let range = target.range.to_offset(target.buffer.read(cx));
let range = editor.range_for_match(&range);
let range = target.range.to_offset(target.buffer.read(cx));
let range = editor.range_for_match(&range);
if Some(&target.buffer) == editor.buffer.read(cx).as_singleton().as_ref() {
let buffer = target.buffer.read(cx);
let range = check_multiline_range(buffer, range);
editor.change_selections(Some(Autoscroll::focused()), cx, |s| {
s.select_ranges([range]);
});
} else {
cx.window_context().defer(move |cx| {
let target_editor: View<Self> =
workspace.update(cx, |workspace, cx| {
let pane = if split {
workspace.adjacent_pane(cx)
} else {
workspace.active_pane().clone()
};
/// If select range has more than one line, we
/// just point the cursor to range.start.
fn check_multiline_range(
buffer: &Buffer,
range: Range<usize>,
) -> Range<usize> {
if buffer.offset_to_point(range.start).row
== buffer.offset_to_point(range.end).row
{
range
} else {
range.start..range.start
}
}
workspace.open_project_item(
pane,
target.buffer.clone(),
true,
true,
cx,
)
});
target_editor.update(cx, |target_editor, cx| {
// When selecting a definition in a different buffer, disable the nav history
// to avoid creating a history entry at the previous cursor location.
pane.update(cx, |pane, _| pane.disable_history());
let buffer = target.buffer.read(cx);
let range = check_multiline_range(buffer, range);
target_editor.change_selections(
Some(Autoscroll::focused()),
cx,
|s| {
s.select_ranges([range]);
},
);
pane.update(cx, |pane, _| pane.enable_history());
if Some(&target.buffer) == editor.buffer.read(cx).as_singleton().as_ref() {
let buffer = target.buffer.read(cx);
let range = check_multiline_range(buffer, range);
editor.change_selections(Some(Autoscroll::focused()), cx, |s| {
s.select_ranges([range]);
});
});
}
Navigated::Yes
})
} else {
cx.window_context().defer(move |cx| {
let target_editor: View<Self> =
workspace.update(cx, |workspace, cx| {
let pane = if split {
workspace.adjacent_pane(cx)
} else {
workspace.active_pane().clone()
};
workspace.open_project_item(
pane,
target.buffer.clone(),
true,
true,
cx,
)
});
target_editor.update(cx, |target_editor, cx| {
// When selecting a definition in a different buffer, disable the nav history
// to avoid creating a history entry at the previous cursor location.
pane.update(cx, |pane, _| pane.disable_history());
let buffer = target.buffer.read(cx);
let range = check_multiline_range(buffer, range);
target_editor.change_selections(
Some(Autoscroll::focused()),
cx,
|s| {
s.select_ranges([range]);
},
);
pane.update(cx, |pane, _| pane.enable_history());
});
});
}
true
})
} else {
Ok(false)
}
})
} else if !definitions.is_empty() {
let replica_id = self.replica_id(cx);
@@ -9301,7 +9289,7 @@ impl Editor {
.context("location tasks")?;
let Some(workspace) = workspace else {
return Ok(Navigated::No);
return Ok(false);
};
let opened = workspace
.update(&mut cx, |workspace, cx| {
@@ -9311,10 +9299,10 @@ impl Editor {
})
.ok();
anyhow::Ok(Navigated::from_bool(opened.is_some()))
anyhow::Ok(opened.is_some())
})
} else {
Task::ready(Ok(Navigated::No))
Task::ready(Ok(false))
}
}
@@ -9373,7 +9361,7 @@ impl Editor {
&mut self,
_: &FindAllReferences,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<Navigated>>> {
) -> Option<Task<Result<()>>> {
let multi_buffer = self.buffer.read(cx);
let selection = self.selections.newest::<usize>(cx);
let head = selection.head();
@@ -9428,7 +9416,7 @@ impl Editor {
let locations = references.await?;
if locations.is_empty() {
return anyhow::Ok(Navigated::No);
return anyhow::Ok(());
}
workspace.update(&mut cx, |workspace, cx| {
@@ -9448,7 +9436,6 @@ impl Editor {
Self::open_locations_in_multibuffer(
workspace, locations, replica_id, title, false, cx,
);
Navigated::Yes
})
}))
}
@@ -9696,7 +9683,6 @@ impl Editor {
color: Some(cx.theme().status().predictive),
..HighlightStyle::default()
},
..EditorStyle::default()
},
))
.into_any_element()
@@ -11860,6 +11846,7 @@ impl Editor {
self.editor_actions.borrow_mut().insert(
id,
Box::new(move |cx| {
let _view = cx.view().clone();
let cx = cx.window_context();
let listener = listener.clone();
cx.on_action(TypeId::of::<A>(), move |action, phase, cx| {
@@ -11945,22 +11932,6 @@ impl Editor {
menu.visible() && matches!(menu, ContextMenu::Completions(_))
})
}
pub fn register_addon<T: Addon>(&mut self, instance: T) {
self.addons
.insert(std::any::TypeId::of::<T>(), Box::new(instance));
}
pub fn unregister_addon<T: Addon>(&mut self) {
self.addons.remove(&std::any::TypeId::of::<T>());
}
pub fn addon<T: Addon>(&self) -> Option<&T> {
let type_id = std::any::TypeId::of::<T>();
self.addons
.get(&type_id)
.and_then(|item| item.to_any().downcast_ref::<T>())
}
}
fn hunks_for_selections(
@@ -12602,7 +12573,6 @@ impl Render for Editor {
color: Some(cx.theme().status().predictive),
..HighlightStyle::default()
},
unnecessary_code_fade: ThemeSettings::get_global(cx).unnecessary_code_fade,
},
)
}
@@ -13307,13 +13277,3 @@ fn hunk_status(hunk: &DiffHunk<MultiBufferRow>) -> DiffHunkStatus {
DiffHunkStatus::Modified
}
}
/// If select range has more than one line, we
/// just point the cursor to range.start.
fn check_multiline_range(buffer: &Buffer, range: Range<usize>) -> Range<usize> {
if buffer.offset_to_point(range.start).row == buffer.offset_to_point(range.end).row {
range
} else {
range.start..range.start
}
}

View File

@@ -9090,43 +9090,6 @@ async fn go_to_prev_overlapping_diagnostic(
"});
}
#[gpui::test]
async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.set_state(indoc! {"
fn func(abˇc def: i32) -> u32 {
}
"});
let project = cx.update_editor(|editor, _| editor.project.clone().unwrap());
cx.update(|cx| {
project.update(cx, |project, cx| {
project.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path("/root/file").unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
..Default::default()
}],
},
&[],
cx,
)
})
}).unwrap();
cx.run_until_parked();
cx.update_editor(|editor, cx| hover_popover::hover(editor, &Default::default(), cx));
cx.run_until_parked();
cx.update_editor(|editor, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
}
#[gpui::test]
async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
@@ -13258,127 +13221,6 @@ let foo = 15;"#,
});
}
#[gpui::test]
async fn test_goto_definition_with_find_all_references_fallback(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
definition_provider: Some(lsp::OneOf::Left(true)),
references_provider: Some(lsp::OneOf::Left(true)),
..lsp::ServerCapabilities::default()
},
cx,
)
.await;
let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
let go_to_definition = cx.lsp.handle_request::<lsp::request::GotoDefinition, _, _>(
move |params, _| async move {
if empty_go_to_definition {
Ok(None)
} else {
Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
uri: params.text_document_position_params.text_document.uri,
range: lsp::Range::new(lsp::Position::new(4, 3), lsp::Position::new(4, 6)),
})))
}
},
);
let references =
cx.lsp
.handle_request::<lsp::request::References, _, _>(move |params, _| async move {
Ok(Some(vec![lsp::Location {
uri: params.text_document_position.text_document.uri,
range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
}]))
});
(go_to_definition, references)
};
cx.set_state(
&r#"fn one() {
let mut a = ˇtwo();
}
fn two() {}"#
.unindent(),
);
set_up_lsp_handlers(false, &mut cx);
let navigated = cx
.update_editor(|editor, cx| editor.go_to_definition(&GoToDefinition, cx))
.await
.expect("Failed to navigate to definition");
assert_eq!(
navigated,
Navigated::Yes,
"Should have navigated to definition from the GetDefinition response"
);
cx.assert_editor_state(
&r#"fn one() {
let mut a = two();
}
fn «twoˇ»() {}"#
.unindent(),
);
let editors = cx.update_workspace(|workspace, cx| {
workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
});
cx.update_editor(|_, test_editor_cx| {
assert_eq!(
editors.len(),
1,
"Initially, only one, test, editor should be open in the workspace"
);
assert_eq!(
test_editor_cx.view(),
editors.last().expect("Asserted len is 1")
);
});
set_up_lsp_handlers(true, &mut cx);
let navigated = cx
.update_editor(|editor, cx| editor.go_to_definition(&GoToDefinition, cx))
.await
.expect("Failed to navigate to lookup references");
assert_eq!(
navigated,
Navigated::Yes,
"Should have navigated to references as a fallback after empty GoToDefinition response"
);
// We should not change the selections in the existing file,
// if opening another milti buffer with the references
cx.assert_editor_state(
&r#"fn one() {
let mut a = two();
}
fn «twoˇ»() {}"#
.unindent(),
);
let editors = cx.update_workspace(|workspace, cx| {
workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
});
cx.update_editor(|_, test_editor_cx| {
assert_eq!(
editors.len(),
2,
"After falling back to references search, we open a new editor with the results"
);
let references_fallback_text = editors
.into_iter()
.find(|new_editor| new_editor != test_editor_cx.view())
.expect("Should have one non-test editor now")
.read(test_editor_cx)
.text(test_editor_cx);
assert_eq!(
references_fallback_text, "fn one() {\n let mut a = two();\n}",
"Should use the range from the references response and not the GoToDefinition one"
);
});
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
point..point

View File

@@ -5613,7 +5613,7 @@ impl Element for EditorElement {
cx: &mut WindowContext,
) {
let focus_handle = self.editor.focus_handle(cx);
let key_context = self.editor.update(cx, |editor, cx| editor.key_context(cx));
let key_context = self.editor.read(cx).key_context(cx);
cx.set_key_context(key_context);
cx.handle_input(
&focus_handle,

View File

@@ -2,7 +2,7 @@ use crate::{
hover_popover::{self, InlayHover},
scroll::ScrollAmount,
Anchor, Editor, EditorSnapshot, FindAllReferences, GoToDefinition, GoToTypeDefinition, InlayId,
Navigated, PointForPosition, SelectPhase,
PointForPosition, SelectPhase,
};
use gpui::{px, AppContext, AsyncWindowContext, Model, Modifiers, Task, ViewContext};
use language::{Bias, ToOffset};
@@ -157,10 +157,10 @@ impl Editor {
) {
let reveal_task = self.cmd_click_reveal_task(point, modifiers, cx);
cx.spawn(|editor, mut cx| async move {
let definition_revealed = reveal_task.await.log_err().unwrap_or(Navigated::No);
let definition_revealed = reveal_task.await.log_err().unwrap_or(false);
let find_references = editor
.update(&mut cx, |editor, cx| {
if definition_revealed == Navigated::Yes {
if definition_revealed {
return None;
}
editor.find_all_references(&FindAllReferences, cx)
@@ -194,7 +194,7 @@ impl Editor {
point: PointForPosition,
modifiers: Modifiers,
cx: &mut ViewContext<Editor>,
) -> Task<anyhow::Result<Navigated>> {
) -> Task<anyhow::Result<bool>> {
if let Some(hovered_link_state) = self.hovered_link_state.take() {
self.hide_hovered_link(cx);
if !hovered_link_state.links.is_empty() {
@@ -211,7 +211,7 @@ impl Editor {
.read(cx)
.text_anchor_for_position(current_position, cx)
else {
return Task::ready(Ok(Navigated::No));
return Task::ready(Ok(false));
};
let links = hovered_link_state
.links
@@ -247,7 +247,7 @@ impl Editor {
self.go_to_definition(&GoToDefinition, cx)
}
} else {
Task::ready(Ok(Navigated::No))
Task::ready(Ok(false))
}
}
}

View File

@@ -680,12 +680,6 @@ impl Item for Editor {
self.nav_history = Some(history);
}
fn discarded(&self, _project: Model<Project>, cx: &mut ViewContext<Self>) {
for buffer in self.buffer().clone().read(cx).all_buffers() {
buffer.update(cx, |buffer, cx| buffer.discarded(cx))
}
}
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
let selection = self.selections.newest_anchor();
self.push_to_nav_history(selection.head(), None, cx);

View File

@@ -43,11 +43,6 @@ impl FeatureFlag for LanguageModels {
const NAME: &'static str = "language-models";
}
pub struct LlmClosedBeta {}
impl FeatureFlag for LlmClosedBeta {
const NAME: &'static str = "llm-closed-beta";
}
pub struct ZedPro {}
impl FeatureFlag for ZedPro {
const NAME: &'static str = "zed-pro";

View File

@@ -20,6 +20,7 @@ futures.workspace = true
git.workspace = true
git2.workspace = true
gpui.workspace = true
lazy_static.workspace = true
libc.workspace = true
parking_lot.workspace = true
paths.workspace = true

View File

@@ -794,8 +794,9 @@ impl FakeFsState {
}
#[cfg(any(test, feature = "test-support"))]
pub static FS_DOT_GIT: std::sync::LazyLock<&'static OsStr> =
std::sync::LazyLock::new(|| OsStr::new(".git"));
lazy_static::lazy_static! {
pub static ref FS_DOT_GIT: &'static OsStr = OsStr::new(".git");
}
#[cfg(any(test, feature = "test-support"))]
impl FakeFs {

View File

@@ -20,6 +20,7 @@ derive_more.workspace = true
git2.workspace = true
gpui.workspace = true
http_client.workspace = true
lazy_static.workspace = true
log.workspace = true
parking_lot.workspace = true
rope.workspace = true

View File

@@ -5,9 +5,9 @@ use serde::{Deserialize, Serialize};
use std::ffi::OsStr;
use std::fmt;
use std::str::FromStr;
use std::sync::LazyLock;
pub use git2 as libgit;
pub use lazy_static::lazy_static;
pub use crate::hosting_provider::*;
@@ -17,8 +17,10 @@ pub mod diff;
pub mod repository;
pub mod status;
pub static DOT_GIT: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".git"));
pub static GITIGNORE: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".gitignore"));
lazy_static! {
pub static ref DOT_GIT: &'static OsStr = OsStr::new(".git");
pub static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore");
}
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct Oid(libgit::Oid);

View File

@@ -12,11 +12,11 @@ use ui::{
use util::paths::FILE_ROW_COLUMN_DELIMITER;
use workspace::{item::ItemHandle, StatusItemView, Workspace};
#[derive(Copy, Clone, Debug, Default, PartialOrd, PartialEq)]
pub(crate) struct SelectionStats {
pub lines: usize,
pub characters: usize,
pub selections: usize,
#[derive(Copy, Clone, Default, PartialOrd, PartialEq)]
struct SelectionStats {
lines: usize,
characters: usize,
selections: usize,
}
pub struct CursorPosition {
@@ -44,10 +44,7 @@ impl CursorPosition {
self.selected_count.selections = editor.selections.count();
let mut last_selection: Option<Selection<usize>> = None;
for selection in editor.selections.all::<usize>(cx) {
self.selected_count.characters += buffer
.text_for_range(selection.start..selection.end)
.map(|t| t.chars().count())
.sum::<usize>();
self.selected_count.characters += selection.end - selection.start;
if last_selection
.as_ref()
.map_or(true, |last_selection| selection.id > last_selection.id)
@@ -109,11 +106,6 @@ impl CursorPosition {
}
text.push(')');
}
#[cfg(test)]
pub(crate) fn selection_stats(&self) -> &SelectionStats {
&self.selected_count
}
}
impl Render for CursorPosition {

View File

@@ -221,8 +221,6 @@ impl Render for GoToLine {
#[cfg(test)]
mod tests {
use super::*;
use cursor_position::{CursorPosition, SelectionStats};
use editor::actions::SelectAll;
use gpui::{TestAppContext, VisualTestContext};
use indoc::indoc;
use project::{FakeFs, Project};
@@ -337,83 +335,6 @@ mod tests {
assert_single_caret_at_row(&editor, expected_highlighted_row, cx);
}
#[gpui::test]
async fn test_unicode_characters_selection(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/dir",
json!({
"a.rs": "ēlo"
}),
)
.await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
workspace.update(cx, |workspace, cx| {
let cursor_position = cx.new_view(|_| CursorPosition::new(workspace));
workspace.status_bar().update(cx, |status_bar, cx| {
status_bar.add_right_item(cursor_position, cx);
});
});
let worktree_id = workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
})
});
let _buffer = project
.update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
.await
.unwrap();
let editor = workspace
.update(cx, |workspace, cx| {
workspace.open_path((worktree_id, "a.rs"), None, true, cx)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
workspace.update(cx, |workspace, cx| {
assert_eq!(
&SelectionStats {
lines: 0,
characters: 0,
selections: 1,
},
workspace
.status_bar()
.read(cx)
.item_of_type::<CursorPosition>()
.expect("missing cursor position item")
.read(cx)
.selection_stats(),
"No selections should be initially"
);
});
editor.update(cx, |editor, cx| editor.select_all(&SelectAll, cx));
workspace.update(cx, |workspace, cx| {
assert_eq!(
&SelectionStats {
lines: 1,
characters: 3,
selections: 1,
},
workspace
.status_bar()
.read(cx)
.item_of_type::<CursorPosition>()
.expect("missing cursor position item")
.read(cx)
.selection_stats(),
"After selecting a text with multibyte unicode characters, the character count should be correct"
);
});
}
fn open_go_to_line_view(
workspace: &View<Workspace>,
cx: &mut VisualTestContext,

View File

@@ -43,6 +43,7 @@ gpui_macros.workspace = true
http_client.workspace = true
image = "0.25.1"
itertools.workspace = true
lazy_static.workspace = true
linkme = "0.3"
log.workspace = true
num_cpus = "1.13"

View File

@@ -6,7 +6,7 @@ use std::{
path::{Path, PathBuf},
rc::{Rc, Weak},
sync::{atomic::Ordering::SeqCst, Arc},
time::{Duration, Instant},
time::Duration,
};
use anyhow::{anyhow, Result};
@@ -142,12 +142,6 @@ impl App {
self
}
/// Sets a start time for tracking time to first window draw.
pub fn measure_time_to_first_window_draw(self, start: Instant) -> Self {
self.0.borrow_mut().time_to_first_window_draw = Some(TimeToFirstWindowDraw::Pending(start));
self
}
/// Start the application. The provided callback will be called once the
/// app is fully launched.
pub fn run<F>(self, on_finish_launching: F)
@@ -253,7 +247,6 @@ pub struct AppContext {
pub(crate) layout_id_buffer: Vec<LayoutId>, // We recycle this memory across layout requests.
pub(crate) propagate_event: bool,
pub(crate) prompt_builder: Option<PromptBuilder>,
pub(crate) time_to_first_window_draw: Option<TimeToFirstWindowDraw>,
}
impl AppContext {
@@ -307,7 +300,6 @@ impl AppContext {
layout_id_buffer: Default::default(),
propagate_event: true,
prompt_builder: Some(PromptBuilder::Default),
time_to_first_window_draw: None,
}),
});
@@ -1310,14 +1302,6 @@ impl AppContext {
(task, is_first)
}
/// Returns the time to first window draw, if available.
pub fn time_to_first_window_draw(&self) -> Option<Duration> {
match self.time_to_first_window_draw {
Some(TimeToFirstWindowDraw::Done(duration)) => Some(duration),
_ => None,
}
}
}
impl Context for AppContext {
@@ -1481,15 +1465,6 @@ impl<G: Global> DerefMut for GlobalLease<G> {
}
}
/// Represents the initialization duration of the application.
#[derive(Clone, Copy)]
pub enum TimeToFirstWindowDraw {
/// The application is still initializing, and contains the start time.
Pending(Instant),
/// The application has finished initializing, and contains the total duration.
Done(Duration),
}
/// Contains state associated with an active drag operation, started by dragging an element
/// within the window or by dragging into the app from the underlying platform.
pub struct AnyDrag {

View File

@@ -642,8 +642,10 @@ impl<T> PartialEq<Model<T>> for WeakModel<T> {
}
#[cfg(any(test, feature = "test-support"))]
static LEAK_BACKTRACE: std::sync::LazyLock<bool> =
std::sync::LazyLock::new(|| std::env::var("LEAK_BACKTRACE").map_or(false, |b| !b.is_empty()));
lazy_static::lazy_static! {
static ref LEAK_BACKTRACE: bool =
std::env::var("LEAK_BACKTRACE").map_or(false, |b| !b.is_empty());
}
#[cfg(any(test, feature = "test-support"))]
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]

View File

@@ -16,7 +16,6 @@ mod blade;
#[cfg(any(test, feature = "test-support"))]
mod test;
mod fps;
#[cfg(target_os = "windows")]
mod windows;
@@ -52,7 +51,6 @@ use strum::EnumIter;
use uuid::Uuid;
pub use app_menu::*;
pub use fps::*;
pub use keystroke::*;
#[cfg(target_os = "linux")]
@@ -356,7 +354,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
fn on_close(&self, callback: Box<dyn FnOnce()>);
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
fn draw(&self, scene: &Scene, on_complete: Option<oneshot::Sender<()>>);
fn draw(&self, scene: &Scene);
fn completed_frame(&self) {}
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
@@ -381,7 +379,6 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
}
fn set_client_inset(&self, _inset: Pixels) {}
fn gpu_specs(&self) -> Option<GPUSpecs>;
fn fps(&self) -> Option<f32>;
#[cfg(any(test, feature = "test-support"))]
fn as_test(&mut self) -> Option<&mut TestWindow> {
@@ -940,11 +937,11 @@ pub enum CursorStyle {
ResizeUpRightDownLeft,
/// A cursor indicating that the item/column can be resized horizontally.
/// corresponds to the CSS cursor value `col-resize`
/// corresponds to the CSS curosr value `col-resize`
ResizeColumn,
/// A cursor indicating that the item/row can be resized vertically.
/// corresponds to the CSS cursor value `row-resize`
/// corresponds to the CSS curosr value `row-resize`
ResizeRow,
/// A text input cursor for vertical layout

View File

@@ -9,7 +9,6 @@ use crate::{
};
use bytemuck::{Pod, Zeroable};
use collections::HashMap;
use futures::channel::oneshot;
#[cfg(target_os = "macos")]
use media::core_video::CVMetalTextureCache;
#[cfg(target_os = "macos")]
@@ -538,12 +537,7 @@ impl BladeRenderer {
self.gpu.destroy_command_encoder(&mut self.command_encoder);
}
pub fn draw(
&mut self,
scene: &Scene,
// Required to compile on macOS, but not currently supported.
_on_complete: Option<oneshot::Sender<()>>,
) {
pub fn draw(&mut self, scene: &Scene) {
self.command_encoder.start();
self.atlas.before_frame(&mut self.command_encoder);
self.rasterize_paths(scene.paths());
@@ -772,9 +766,4 @@ impl BladeRenderer {
self.wait_for_gpu();
self.last_sync_point = Some(sync_point);
}
/// Required to compile on macOS, but not currently supported.
pub fn fps(&self) -> f32 {
0.0
}
}

View File

@@ -1,94 +0,0 @@
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;
const NANOS_PER_SEC: u64 = 1_000_000_000;
const WINDOW_SIZE: usize = 128;
/// Represents a rolling FPS (Frames Per Second) counter.
///
/// This struct provides a lock-free mechanism to measure and calculate FPS
/// continuously, updating with every frame. It uses atomic operations to
/// ensure thread-safety without the need for locks.
pub struct FpsCounter {
frame_times: [AtomicU64; WINDOW_SIZE],
head: AtomicUsize,
tail: AtomicUsize,
}
impl FpsCounter {
/// Creates a new `Fps` counter.
///
/// Returns an `Arc<Fps>` for safe sharing across threads.
pub fn new() -> Arc<Self> {
Arc::new(Self {
frame_times: std::array::from_fn(|_| AtomicU64::new(0)),
head: AtomicUsize::new(0),
tail: AtomicUsize::new(0),
})
}
/// Increments the FPS counter with a new frame timestamp.
///
/// This method updates the internal state to maintain a rolling window
/// of frame data for the last second. It uses atomic operations to
/// ensure thread-safety.
///
/// # Arguments
///
/// * `timestamp_ns` - The timestamp of the new frame in nanoseconds.
pub fn increment(&self, timestamp_ns: u64) {
let mut head = self.head.load(Ordering::Relaxed);
let mut tail = self.tail.load(Ordering::Relaxed);
// Add new timestamp
self.frame_times[head].store(timestamp_ns, Ordering::Relaxed);
// Increment head and wrap around to 0 if it reaches WINDOW_SIZE
head = (head + 1) % WINDOW_SIZE;
self.head.store(head, Ordering::Relaxed);
// Remove old timestamps (older than 1 second)
while tail != head {
let oldest = self.frame_times[tail].load(Ordering::Relaxed);
if timestamp_ns.wrapping_sub(oldest) <= NANOS_PER_SEC {
break;
}
// Increment tail and wrap around to 0 if it reaches WINDOW_SIZE
tail = (tail + 1) % WINDOW_SIZE;
self.tail.store(tail, Ordering::Relaxed);
}
}
/// Calculates and returns the current FPS.
///
/// This method computes the FPS based on the frames recorded in the last second.
/// It uses atomic loads to ensure thread-safety.
///
/// # Returns
///
/// The calculated FPS as a `f32`, or 0.0 if no frames have been recorded.
pub fn fps(&self) -> f32 {
let head = self.head.load(Ordering::Relaxed);
let tail = self.tail.load(Ordering::Relaxed);
if head == tail {
return 0.0;
}
let newest =
self.frame_times[head.wrapping_sub(1) & (WINDOW_SIZE - 1)].load(Ordering::Relaxed);
let oldest = self.frame_times[tail].load(Ordering::Relaxed);
let time_diff = newest.wrapping_sub(oldest) as f32;
if time_diff == 0.0 {
return 0.0;
}
let frame_count = if head > tail {
head - tail
} else {
WINDOW_SIZE - tail + head
};
(frame_count as f32 - 1.0) * NANOS_PER_SEC as f32 / time_diff
}
}

View File

@@ -6,7 +6,7 @@ use std::sync::Arc;
use blade_graphics as gpu;
use collections::HashMap;
use futures::channel::oneshot;
use futures::channel::oneshot::Receiver;
use raw_window_handle as rwh;
use wayland_backend::client::ObjectId;
@@ -827,7 +827,7 @@ impl PlatformWindow for WaylandWindow {
_msg: &str,
_detail: Option<&str>,
_answers: &[&str],
) -> Option<oneshot::Receiver<usize>> {
) -> Option<Receiver<usize>> {
None
}
@@ -934,9 +934,9 @@ impl PlatformWindow for WaylandWindow {
self.0.callbacks.borrow_mut().appearance_changed = Some(callback);
}
fn draw(&self, scene: &Scene, on_complete: Option<oneshot::Sender<()>>) {
fn draw(&self, scene: &Scene) {
let mut state = self.borrow_mut();
state.renderer.draw(scene, on_complete);
state.renderer.draw(scene);
}
fn completed_frame(&self) {
@@ -1009,10 +1009,6 @@ impl PlatformWindow for WaylandWindow {
fn gpu_specs(&self) -> Option<GPUSpecs> {
self.borrow().renderer.gpu_specs().into()
}
fn fps(&self) -> Option<f32> {
None
}
}
fn update_window(mut state: RefMut<WaylandWindowState>) {

View File

@@ -1,3 +1,5 @@
use anyhow::Context;
use crate::{
platform::blade::{BladeRenderer, BladeSurfaceConfig},
px, size, AnyWindowHandle, Bounds, Decorations, DevicePixels, ForegroundExecutor, GPUSpecs,
@@ -7,9 +9,7 @@ use crate::{
X11ClientStatePtr,
};
use anyhow::Context;
use blade_graphics as gpu;
use futures::channel::oneshot;
use raw_window_handle as rwh;
use util::{maybe, ResultExt};
use x11rb::{
@@ -1210,10 +1210,9 @@ impl PlatformWindow for X11Window {
self.0.callbacks.borrow_mut().appearance_changed = Some(callback);
}
// TODO: on_complete not yet supported for X11 windows
fn draw(&self, scene: &Scene, on_complete: Option<oneshot::Sender<()>>) {
fn draw(&self, scene: &Scene) {
let mut inner = self.0.state.borrow_mut();
inner.renderer.draw(scene, on_complete);
inner.renderer.draw(scene);
}
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
@@ -1399,8 +1398,4 @@ impl PlatformWindow for X11Window {
fn gpu_specs(&self) -> Option<GPUSpecs> {
self.0.state.borrow().renderer.gpu_specs().into()
}
fn fps(&self) -> Option<f32> {
None
}
}

View File

@@ -1,7 +1,7 @@
use super::metal_atlas::MetalAtlas;
use crate::{
point, size, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, ContentMask, DevicePixels,
FpsCounter, Hsla, MonochromeSprite, PaintSurface, Path, PathId, PathVertex, PolychromeSprite,
Hsla, MonochromeSprite, PaintSurface, Path, PathId, PathVertex, PolychromeSprite,
PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline,
};
use anyhow::{anyhow, Result};
@@ -14,7 +14,6 @@ use cocoa::{
use collections::HashMap;
use core_foundation::base::TCFType;
use foreign_types::ForeignType;
use futures::channel::oneshot;
use media::core_video::CVMetalTextureCache;
use metal::{CAMetalLayer, CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange};
use objc::{self, msg_send, sel, sel_impl};
@@ -106,7 +105,6 @@ pub(crate) struct MetalRenderer {
instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>,
sprite_atlas: Arc<MetalAtlas>,
core_video_texture_cache: CVMetalTextureCache,
fps_counter: Arc<FpsCounter>,
}
impl MetalRenderer {
@@ -252,7 +250,6 @@ impl MetalRenderer {
instance_buffer_pool,
sprite_atlas,
core_video_texture_cache,
fps_counter: FpsCounter::new(),
}
}
@@ -295,8 +292,7 @@ impl MetalRenderer {
// nothing to do
}
pub fn draw(&mut self, scene: &Scene, on_complete: Option<oneshot::Sender<()>>) {
let on_complete = Arc::new(Mutex::new(on_complete));
pub fn draw(&mut self, scene: &Scene) {
let layer = self.layer.clone();
let viewport_size = layer.drawable_size();
let viewport_size: Size<DevicePixels> = size(
@@ -323,24 +319,13 @@ impl MetalRenderer {
Ok(command_buffer) => {
let instance_buffer_pool = self.instance_buffer_pool.clone();
let instance_buffer = Cell::new(Some(instance_buffer));
let device = self.device.clone();
let fps_counter = self.fps_counter.clone();
let completed_handler =
ConcreteBlock::new(move |_: &metal::CommandBufferRef| {
let mut cpu_timestamp = 0;
let mut gpu_timestamp = 0;
device.sample_timestamps(&mut cpu_timestamp, &mut gpu_timestamp);
fps_counter.increment(gpu_timestamp);
if let Some(on_complete) = on_complete.lock().take() {
on_complete.send(()).ok();
}
if let Some(instance_buffer) = instance_buffer.take() {
instance_buffer_pool.lock().release(instance_buffer);
}
});
let completed_handler = completed_handler.copy();
command_buffer.add_completed_handler(&completed_handler);
let block = ConcreteBlock::new(move |_| {
if let Some(instance_buffer) = instance_buffer.take() {
instance_buffer_pool.lock().release(instance_buffer);
}
});
let block = block.copy();
command_buffer.add_completed_handler(&block);
if self.presents_with_transaction {
command_buffer.commit();
@@ -1132,10 +1117,6 @@ impl MetalRenderer {
}
true
}
pub fn fps(&self) -> f32 {
self.fps_counter.fps()
}
}
fn build_pipeline_state(

View File

@@ -784,14 +784,14 @@ impl PlatformWindow for MacWindow {
self.0.as_ref().lock().bounds()
}
fn is_maximized(&self) -> bool {
self.0.as_ref().lock().is_maximized()
}
fn window_bounds(&self) -> WindowBounds {
self.0.as_ref().lock().window_bounds()
}
fn is_maximized(&self) -> bool {
self.0.as_ref().lock().is_maximized()
}
fn content_size(&self) -> Size<Pixels> {
self.0.as_ref().lock().content_size()
}
@@ -975,6 +975,8 @@ impl PlatformWindow for MacWindow {
}
}
fn set_app_id(&mut self, _app_id: &str) {}
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
let mut this = self.0.as_ref().lock();
this.renderer
@@ -1005,6 +1007,30 @@ impl PlatformWindow for MacWindow {
}
}
fn set_edited(&mut self, edited: bool) {
unsafe {
let window = self.0.lock().native_window;
msg_send![window, setDocumentEdited: edited as BOOL]
}
// Changing the document edited state resets the traffic light position,
// so we have to move it again.
self.0.lock().move_traffic_light();
}
fn show_character_palette(&self) {
let this = self.0.lock();
let window = this.native_window;
this.executor
.spawn(async move {
unsafe {
let app = NSApplication::sharedApplication(nil);
let _: () = msg_send![app, orderFrontCharacterPalette: window];
}
})
.detach();
}
fn minimize(&self) {
let window = self.0.lock().native_window;
unsafe {
@@ -1081,48 +1107,18 @@ impl PlatformWindow for MacWindow {
self.0.lock().appearance_changed_callback = Some(callback);
}
fn draw(&self, scene: &crate::Scene, on_complete: Option<oneshot::Sender<()>>) {
fn draw(&self, scene: &crate::Scene) {
let mut this = self.0.lock();
this.renderer.draw(scene, on_complete);
this.renderer.draw(scene);
}
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
self.0.lock().renderer.sprite_atlas().clone()
}
fn set_edited(&mut self, edited: bool) {
unsafe {
let window = self.0.lock().native_window;
msg_send![window, setDocumentEdited: edited as BOOL]
}
// Changing the document edited state resets the traffic light position,
// so we have to move it again.
self.0.lock().move_traffic_light();
}
fn show_character_palette(&self) {
let this = self.0.lock();
let window = this.native_window;
this.executor
.spawn(async move {
unsafe {
let app = NSApplication::sharedApplication(nil);
let _: () = msg_send![app, orderFrontCharacterPalette: window];
}
})
.detach();
}
fn set_app_id(&mut self, _app_id: &str) {}
fn gpu_specs(&self) -> Option<crate::GPUSpecs> {
None
}
fn fps(&self) -> Option<f32> {
Some(self.0.lock().renderer.fps())
}
}
impl rwh::HasWindowHandle for MacWindow {

View File

@@ -251,12 +251,7 @@ impl PlatformWindow for TestWindow {
fn on_appearance_changed(&self, _callback: Box<dyn FnMut()>) {}
fn draw(
&self,
_scene: &crate::Scene,
_on_complete: Option<futures::channel::oneshot::Sender<()>>,
) {
}
fn draw(&self, _scene: &crate::Scene) {}
fn sprite_atlas(&self) -> sync::Arc<dyn crate::PlatformAtlas> {
self.0.lock().sprite_atlas.clone()
@@ -282,10 +277,6 @@ impl PlatformWindow for TestWindow {
fn gpu_specs(&self) -> Option<GPUSpecs> {
None
}
fn fps(&self) -> Option<f32> {
None
}
}
pub(crate) struct TestAtlasState {

View File

@@ -660,8 +660,8 @@ impl PlatformWindow for WindowsWindow {
self.0.state.borrow_mut().callbacks.appearance_changed = Some(callback);
}
fn draw(&self, scene: &Scene, on_complete: Option<oneshot::Sender<()>>) {
self.0.state.borrow_mut().renderer.draw(scene, on_complete)
fn draw(&self, scene: &Scene) {
self.0.state.borrow_mut().renderer.draw(scene)
}
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
@@ -675,10 +675,6 @@ impl PlatformWindow for WindowsWindow {
fn gpu_specs(&self) -> Option<GPUSpecs> {
Some(self.0.state.borrow().renderer.gpu_specs())
}
fn fps(&self) -> Option<f32> {
None
}
}
#[implement(IDropTarget)]

View File

@@ -11,9 +11,9 @@ use crate::{
PromptLevel, Quad, Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams,
Replay, ResizeEdge, ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style,
SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement,
TimeToFirstWindowDraw, TransformationMatrix, Underline, UnderlineStyle, View, VisualContext,
WeakView, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls,
WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, SUBPIXEL_VARIANTS,
TransformationMatrix, Underline, UnderlineStyle, View, VisualContext, WeakView,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations,
WindowOptions, WindowParams, WindowTextSystem, SUBPIXEL_VARIANTS,
};
use anyhow::{anyhow, Context as _, Result};
use collections::{FxHashMap, FxHashSet};
@@ -544,8 +544,6 @@ pub struct Window {
hovered: Rc<Cell<bool>>,
pub(crate) dirty: Rc<Cell<bool>>,
pub(crate) needs_present: Rc<Cell<bool>>,
/// We assign this to be notified when the platform graphics backend fires the next completion callback for drawing the window.
present_completed: RefCell<Option<oneshot::Sender<()>>>,
pub(crate) last_input_timestamp: Rc<Cell<Instant>>,
pub(crate) refreshing: bool,
pub(crate) draw_phase: DrawPhase,
@@ -822,7 +820,6 @@ impl Window {
hovered,
dirty,
needs_present,
present_completed: RefCell::default(),
last_input_timestamp,
refreshing: false,
draw_phase: DrawPhase::None,
@@ -1492,29 +1489,13 @@ impl<'a> WindowContext<'a> {
self.window.refreshing = false;
self.window.draw_phase = DrawPhase::None;
self.window.needs_present.set(true);
if let Some(TimeToFirstWindowDraw::Pending(start)) = self.app.time_to_first_window_draw {
let (tx, rx) = oneshot::channel();
*self.window.present_completed.borrow_mut() = Some(tx);
self.spawn(|mut cx| async move {
rx.await.ok();
cx.update(|cx| {
let duration = start.elapsed();
cx.time_to_first_window_draw = Some(TimeToFirstWindowDraw::Done(duration));
log::info!("time to first window draw: {:?}", duration);
cx.push_effect(Effect::Refresh);
})
})
.detach();
}
}
#[profiling::function]
fn present(&self) {
let on_complete = self.window.present_completed.take();
self.window
.platform_window
.draw(&self.window.rendered_frame.scene, on_complete);
.draw(&self.window.rendered_frame.scene);
self.window.needs_present.set(false);
profiling::finish_frame!();
}
@@ -3737,12 +3718,6 @@ impl<'a> WindowContext<'a> {
pub fn gpu_specs(&self) -> Option<GPUSpecs> {
self.window.platform_window.gpu_specs()
}
/// Get the current FPS (frames per second) of the window.
/// This is only supported on macOS currently.
pub fn fps(&self) -> Option<f32> {
self.window.platform_window.fps()
}
}
#[cfg(target_os = "windows")]

View File

@@ -37,6 +37,7 @@ globset.workspace = true
gpui.workspace = true
http_client.workspace = true
itertools.workspace = true
lazy_static.workspace = true
log.workspace = true
lsp.workspace = true
parking_lot.workspace = true

View File

@@ -24,6 +24,7 @@ use gpui::{
AnyElement, AppContext, EventEmitter, HighlightStyle, ModelContext, Task, TaskLabel,
WindowContext,
};
use lazy_static::lazy_static;
use lsp::LanguageServerId;
use parking_lot::Mutex;
use serde_json::Value;
@@ -43,7 +44,7 @@ use std::{
ops::{Deref, Range},
path::{Path, PathBuf},
str,
sync::{Arc, LazyLock},
sync::Arc,
time::{Duration, Instant, SystemTime},
vec,
};
@@ -66,9 +67,11 @@ pub use {tree_sitter_rust, tree_sitter_typescript};
pub use lsp::DiagnosticSeverity;
/// A label for the background task spawned by the buffer to compute
/// a diff against the contents of its file.
pub static BUFFER_DIFF_TASK: LazyLock<TaskLabel> = LazyLock::new(|| TaskLabel::new());
lazy_static! {
/// A label for the background task spawned by the buffer to compute
/// a diff against the contents of its file.
pub static ref BUFFER_DIFF_TASK: TaskLabel = TaskLabel::new();
}
/// Indicate whether a [Buffer] has permissions to edit.
#[derive(PartialEq, Clone, Copy, Debug)]
@@ -329,8 +332,6 @@ pub enum Event {
CapabilityChanged,
/// The buffer was explicitly requested to close.
Closed,
/// The buffer was discarded when closing.
Discarded,
}
/// The file associated with a buffer.
@@ -826,12 +827,6 @@ impl Buffer {
cx.notify();
}
/// This method is called to signal that the buffer has been discarded.
pub fn discarded(&mut self, cx: &mut ModelContext<Self>) {
cx.emit(Event::Discarded);
cx.notify();
}
/// Reloads the contents of the buffer from disk.
pub fn reload(
&mut self,

View File

@@ -16,7 +16,6 @@ use settings::SettingsStore;
use std::{
env,
ops::Range,
sync::LazyLock,
time::{Duration, Instant},
};
use text::network::Network;
@@ -25,12 +24,12 @@ use text::{Point, ToPoint};
use unindent::Unindent as _;
use util::{assert_set_eq, post_inc, test::marked_text_ranges, RandomCharIter};
pub static TRAILING_WHITESPACE_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
RegexBuilder::new(r"[ \t]+$")
lazy_static! {
static ref TRAILING_WHITESPACE_REGEX: Regex = RegexBuilder::new("[ \t]+$")
.multi_line(true)
.build()
.expect("Failed to create TRAILING_WHITESPACE_REGEX")
});
.unwrap();
}
#[cfg(test)]
#[ctor::ctor]

View File

@@ -28,6 +28,7 @@ use futures::Future;
use gpui::{AppContext, AsyncAppContext, Model, SharedString, Task};
pub use highlight_map::HighlightMap;
use http_client::HttpClient;
use lazy_static::lazy_static;
use lsp::{CodeActionKind, LanguageServerBinary};
use parking_lot::Mutex;
use regex::Regex;
@@ -52,7 +53,7 @@ use std::{
str,
sync::{
atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst},
Arc, LazyLock,
Arc,
},
};
use syntax_map::{QueryCursorHandle, SyntaxSnapshot};
@@ -110,23 +111,23 @@ where
func(cursor.deref_mut())
}
static NEXT_LANGUAGE_ID: LazyLock<AtomicUsize> = LazyLock::new(Default::default);
static NEXT_GRAMMAR_ID: LazyLock<AtomicUsize> = LazyLock::new(Default::default);
static WASM_ENGINE: LazyLock<wasmtime::Engine> = LazyLock::new(|| {
wasmtime::Engine::new(&wasmtime::Config::new()).expect("Failed to create Wasmtime engine")
});
lazy_static! {
static ref NEXT_LANGUAGE_ID: AtomicUsize = Default::default();
static ref NEXT_GRAMMAR_ID: AtomicUsize = Default::default();
static ref WASM_ENGINE: wasmtime::Engine = {
wasmtime::Engine::new(&wasmtime::Config::new()).unwrap()
};
/// A shared grammar for plain text, exposed for reuse by downstream crates.
pub static PLAIN_TEXT: LazyLock<Arc<Language>> = LazyLock::new(|| {
Arc::new(Language::new(
/// A shared grammar for plain text, exposed for reuse by downstream crates.
pub static ref PLAIN_TEXT: Arc<Language> = Arc::new(Language::new(
LanguageConfig {
name: "Plain Text".into(),
soft_wrap: Some(SoftWrap::EditorWidth),
..Default::default()
},
None,
))
});
));
}
/// Types that represent a position in a buffer, and can be converted into
/// an LSP position, to send to a language server.

View File

@@ -14,7 +14,7 @@ pub struct Outline<T> {
path_candidate_prefixes: Vec<usize>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct OutlineItem<T> {
pub depth: usize,
pub range: Range<T>,
@@ -88,7 +88,12 @@ impl<T> Outline<T> {
}
/// Find the most similar symbol to the provided query using normalized Levenshtein distance.
pub fn find_most_similar(&self, query: &str) -> Option<(SymbolPath, &OutlineItem<T>)> {
pub fn find_most_similar(&self, symbol: &str) -> Option<(SymbolPath, &OutlineItem<T>)> {
// Sometimes the model incorrectly includes a space or colon in the query,
// e.g. in Elm, Roc, etc. it might say `foo : ...` because it's trying to include the symbol's type,
// which isn't part of the symbol and won't be in the outline. Trim the first space and anything after it.
let symbol = &symbol[..symbol.find(' ').unwrap_or(symbol.len())];
const SIMILARITY_THRESHOLD: f64 = 0.6;
let (position, similarity) = self
@@ -96,7 +101,7 @@ impl<T> Outline<T> {
.iter()
.enumerate()
.map(|(index, candidate)| {
let similarity = strsim::normalized_levenshtein(&candidate.string, query);
let similarity = strsim::normalized_levenshtein(&candidate.string, symbol);
(index, similarity)
})
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())?;

View File

@@ -54,10 +54,6 @@ pub struct LanguageModelCacheConfiguration {
pub trait LanguageModel: Send + Sync {
fn id(&self) -> LanguageModelId;
fn name(&self) -> LanguageModelName;
/// If None, falls back to [LanguageModelProvider::icon]
fn icon(&self) -> Option<IconName> {
None
}
fn provider_id(&self) -> LanguageModelProviderId;
fn provider_name(&self) -> LanguageModelProviderName;
fn telemetry_id(&self) -> String;

View File

@@ -2,7 +2,6 @@ use proto::Plan;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use strum::EnumIter;
use ui::IconName;
use crate::LanguageModelAvailability;
@@ -66,13 +65,6 @@ impl CloudModel {
}
}
pub fn icon(&self) -> Option<IconName> {
match self {
Self::Anthropic(_) => Some(IconName::AiAnthropicHosted),
_ => None,
}
}
pub fn max_token_count(&self) -> usize {
match self {
Self::Anthropic(model) => model.max_token_count(),

View File

@@ -19,7 +19,7 @@ use settings::{Settings, SettingsStore};
use std::{sync::Arc, time::Duration};
use strum::IntoEnumIterator;
use theme::ThemeSettings;
use ui::{prelude::*, Icon, IconName, Tooltip};
use ui::{prelude::*, Icon, IconName};
use util::ResultExt;
const PROVIDER_ID: &str = "anthropic";
@@ -29,22 +29,15 @@ const PROVIDER_NAME: &str = "Anthropic";
pub struct AnthropicSettings {
pub api_url: String,
pub low_speed_timeout: Option<Duration>,
/// Extend Zed's list of Anthropic models.
pub available_models: Vec<AvailableModel>,
pub needs_setting_migration: bool,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct AvailableModel {
/// The model's name in the Anthropic API. e.g. claude-3-5-sonnet-20240620
pub name: String,
/// The model's name in Zed's UI, such as in the model selector dropdown menu in the assistant panel.
pub display_name: Option<String>,
/// The model's context window size.
pub max_tokens: usize,
/// A model `name` to substitute when calling tools, in case the primary model doesn't support tool calling.
pub tool_override: Option<String>,
/// Configuration of Anthropic's caching API.
pub cache_configuration: Option<LanguageModelCacheConfiguration>,
pub max_output_tokens: Option<u32>,
}
@@ -54,11 +47,8 @@ pub struct AnthropicLanguageModelProvider {
state: gpui::Model<State>,
}
const ANTHROPIC_API_KEY_VAR: &'static str = "ANTHROPIC_API_KEY";
pub struct State {
api_key: Option<String>,
api_key_from_env: bool,
_subscription: Subscription,
}
@@ -70,7 +60,6 @@ impl State {
delete_credentials.await.ok();
this.update(&mut cx, |this, cx| {
this.api_key = None;
this.api_key_from_env = false;
cx.notify();
})
})
@@ -109,20 +98,18 @@ impl State {
.clone();
cx.spawn(|this, mut cx| async move {
let (api_key, from_env) = if let Ok(api_key) = std::env::var(ANTHROPIC_API_KEY_VAR)
{
(api_key, true)
let api_key = if let Ok(api_key) = std::env::var("ANTHROPIC_API_KEY") {
api_key
} else {
let (_, api_key) = cx
.update(|cx| cx.read_credentials(&api_url))?
.await?
.ok_or_else(|| anyhow!("credentials not found"))?;
(String::from_utf8(api_key)?, false)
String::from_utf8(api_key)?
};
this.update(&mut cx, |this, cx| {
this.api_key = Some(api_key);
this.api_key_from_env = from_env;
cx.notify();
})
})
@@ -134,7 +121,6 @@ impl AnthropicLanguageModelProvider {
pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Self {
let state = cx.new_model(|cx| State {
api_key: None,
api_key_from_env: false,
_subscription: cx.observe_global::<SettingsStore>(|_, cx| {
cx.notify();
}),
@@ -185,7 +171,6 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
model.name.clone(),
anthropic::Model::Custom {
name: model.name.clone(),
display_name: model.display_name.clone(),
max_tokens: model.max_tokens,
tool_override: model.tool_override.clone(),
cache_configuration: model.cache_configuration.as_ref().map(|config| {
@@ -544,8 +529,6 @@ impl Render for ConfigurationView {
"Paste your Anthropic API key below and hit enter to use the assistant:",
];
let env_var_set = self.state.read(cx).api_key_from_env;
if self.load_credentials_task.is_some() {
div().child(Label::new("Loading credentials...")).into_any()
} else if self.should_render_editor(cx) {
@@ -567,7 +550,7 @@ impl Render for ConfigurationView {
)
.child(
Label::new(
"You can also assign the {ANTHROPIC_API_KEY_VAR} environment variable and restart Zed.",
"You can also assign the ANTHROPIC_API_KEY environment variable and restart Zed.",
)
.size(LabelSize::Small),
)
@@ -580,21 +563,13 @@ impl Render for ConfigurationView {
h_flex()
.gap_1()
.child(Icon::new(IconName::Check).color(Color::Success))
.child(Label::new(if env_var_set {
format!("API key set in {ANTHROPIC_API_KEY_VAR} environment variable.")
} else {
"API key configured.".to_string()
})),
.child(Label::new("API key configured.")),
)
.child(
Button::new("reset-key", "Reset key")
.icon(Some(IconName::Trash))
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.disabled(env_var_set)
.when(env_var_set, |this| {
this.tooltip(|cx| Tooltip::text(format!("To reset your API key, unset the {ANTHROPIC_API_KEY_VAR} environment variable."), cx))
})
.on_click(cx.listener(|this, _, cx| this.reset_api_key(cx))),
)
.into_any()

View File

@@ -8,7 +8,7 @@ use anthropic::AnthropicError;
use anyhow::{anyhow, Result};
use client::{Client, PerformCompletionParams, UserStore, EXPIRED_LLM_TOKEN_HEADER_NAME};
use collections::BTreeMap;
use feature_flags::{FeatureFlagAppExt, LlmClosedBeta, ZedPro};
use feature_flags::{FeatureFlagAppExt, ZedPro};
use futures::{
future::BoxFuture, stream::BoxStream, AsyncBufReadExt, FutureExt, Stream, StreamExt,
TryStreamExt as _,
@@ -26,10 +26,7 @@ use smol::{
io::{AsyncReadExt, BufReader},
lock::{RwLock, RwLockUpgradableReadGuard, RwLockWriteGuard},
};
use std::{
future,
sync::{Arc, LazyLock},
};
use std::{future, sync::Arc};
use strum::IntoEnumIterator;
use ui::prelude::*;
@@ -40,18 +37,6 @@ use super::anthropic::count_anthropic_tokens;
pub const PROVIDER_ID: &str = "zed.dev";
pub const PROVIDER_NAME: &str = "Zed";
const ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: Option<&str> =
option_env!("ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON");
fn zed_cloud_provider_additional_models() -> &'static [AvailableModel] {
static ADDITIONAL_MODELS: LazyLock<Vec<AvailableModel>> = LazyLock::new(|| {
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON
.map(|json| serde_json::from_str(json).unwrap())
.unwrap_or(Vec::new())
});
ADDITIONAL_MODELS.as_slice()
}
#[derive(Default, Clone, Debug, PartialEq)]
pub struct ZedDotDevSettings {
pub available_models: Vec<AvailableModel>,
@@ -67,20 +52,12 @@ pub enum AvailableProvider {
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct AvailableModel {
/// The provider of the language model.
pub provider: AvailableProvider,
/// The model's name in the provider's API. e.g. claude-3-5-sonnet-20240620
pub name: String,
/// The name displayed in the UI, such as in the assistant panel model dropdown menu.
pub display_name: Option<String>,
/// The size of the context window, indicating the maximum number of tokens the model can process.
pub max_tokens: usize,
/// The maximum number of output tokens allowed by the model.
pub max_output_tokens: Option<u32>,
/// Override this model with a different Anthropic model for tool calls.
pub tool_override: Option<String>,
/// Indicates whether this custom model supports caching.
pub cache_configuration: Option<LanguageModelCacheConfiguration>,
provider: AvailableProvider,
name: String,
max_tokens: usize,
tool_override: Option<String>,
cache_configuration: Option<LanguageModelCacheConfiguration>,
max_output_tokens: Option<u32>,
}
pub struct CloudLanguageModelProvider {
@@ -215,6 +192,39 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
for model in ZedModel::iter() {
models.insert(model.id().to_string(), CloudModel::Zed(model));
}
// Override with available models from settings
for model in &AllLanguageModelSettings::get_global(cx)
.zed_dot_dev
.available_models
{
let model = match model.provider {
AvailableProvider::Anthropic => {
CloudModel::Anthropic(anthropic::Model::Custom {
name: model.name.clone(),
max_tokens: model.max_tokens,
tool_override: model.tool_override.clone(),
cache_configuration: model.cache_configuration.as_ref().map(|config| {
anthropic::AnthropicModelCacheConfiguration {
max_cache_anchors: config.max_cache_anchors,
should_speculate: config.should_speculate,
min_total_token: config.min_total_token,
}
}),
max_output_tokens: model.max_output_tokens,
})
}
AvailableProvider::OpenAi => CloudModel::OpenAi(open_ai::Model::Custom {
name: model.name.clone(),
max_tokens: model.max_tokens,
}),
AvailableProvider::Google => CloudModel::Google(google_ai::Model::Custom {
name: model.name.clone(),
max_tokens: model.max_tokens,
}),
};
models.insert(model.id().to_string(), model.clone());
}
} else {
models.insert(
anthropic::Model::Claude3_5Sonnet.id().to_string(),
@@ -222,48 +232,6 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
);
}
let llm_closed_beta_models = if cx.has_flag::<LlmClosedBeta>() {
zed_cloud_provider_additional_models()
} else {
&[]
};
// Override with available models from settings
for model in AllLanguageModelSettings::get_global(cx)
.zed_dot_dev
.available_models
.iter()
.chain(llm_closed_beta_models)
.cloned()
{
let model = match model.provider {
AvailableProvider::Anthropic => CloudModel::Anthropic(anthropic::Model::Custom {
name: model.name.clone(),
display_name: model.display_name.clone(),
max_tokens: model.max_tokens,
tool_override: model.tool_override.clone(),
cache_configuration: model.cache_configuration.as_ref().map(|config| {
anthropic::AnthropicModelCacheConfiguration {
max_cache_anchors: config.max_cache_anchors,
should_speculate: config.should_speculate,
min_total_token: config.min_total_token,
}
}),
max_output_tokens: model.max_output_tokens,
}),
AvailableProvider::OpenAi => CloudModel::OpenAi(open_ai::Model::Custom {
name: model.name.clone(),
max_tokens: model.max_tokens,
max_output_tokens: model.max_output_tokens,
}),
AvailableProvider::Google => CloudModel::Google(google_ai::Model::Custom {
name: model.name.clone(),
max_tokens: model.max_tokens,
}),
};
models.insert(model.id().to_string(), model.clone());
}
models
.into_values()
.map(|model| {
@@ -421,10 +389,6 @@ impl LanguageModel for CloudLanguageModel {
LanguageModelName::from(self.model.display_name().to_string())
}
fn icon(&self) -> Option<IconName> {
self.model.icon()
}
fn provider_id(&self) -> LanguageModelProviderId {
LanguageModelProviderId(PROVIDER_ID.into())
}
@@ -514,7 +478,7 @@ impl LanguageModel for CloudLanguageModel {
}
CloudModel::OpenAi(model) => {
let client = self.client.clone();
let request = request.into_open_ai(model.id().into(), model.max_output_tokens());
let request = request.into_open_ai(model.id().into());
let llm_api_token = self.llm_api_token.clone();
let future = self.request_limiter.stream(async move {
let response = Self::perform_llm_completion(
@@ -558,7 +522,7 @@ impl LanguageModel for CloudLanguageModel {
}
CloudModel::Zed(model) => {
let client = self.client.clone();
let mut request = request.into_open_ai(model.id().into(), None);
let mut request = request.into_open_ai(model.id().into());
request.max_tokens = Some(4000);
let llm_api_token = self.llm_api_token.clone();
let future = self.request_limiter.stream(async move {
@@ -630,8 +594,7 @@ impl LanguageModel for CloudLanguageModel {
.boxed()
}
CloudModel::OpenAi(model) => {
let mut request =
request.into_open_ai(model.id().into(), model.max_output_tokens());
let mut request = request.into_open_ai(model.id().into());
request.tool_choice = Some(open_ai::ToolChoice::Other(
open_ai::ToolDefinition::Function {
function: open_ai::FunctionDefinition {
@@ -678,7 +641,7 @@ impl LanguageModel for CloudLanguageModel {
}
CloudModel::Zed(model) => {
// All Zed models are OpenAI-based at the time of writing.
let mut request = request.into_open_ai(model.id().into(), None);
let mut request = request.into_open_ai(model.id().into());
request.tool_choice = Some(open_ai::ToolChoice::Other(
open_ai::ToolDefinition::Function {
function: open_ai::FunctionDefinition {

View File

@@ -180,7 +180,6 @@ impl LanguageModel for CopilotChatLanguageModel {
cx: &AppContext,
) -> BoxFuture<'static, Result<usize>> {
let model = match self.model {
CopilotChatModel::Gpt4o => open_ai::Model::FourOmni,
CopilotChatModel::Gpt4 => open_ai::Model::Four,
CopilotChatModel::Gpt3_5Turbo => open_ai::Model::ThreePointFiveTurbo,
};

View File

@@ -14,7 +14,7 @@ use settings::{Settings, SettingsStore};
use std::{future, sync::Arc, time::Duration};
use strum::IntoEnumIterator;
use theme::ThemeSettings;
use ui::{prelude::*, Icon, IconName, Tooltip};
use ui::{prelude::*, Icon, IconName};
use util::ResultExt;
use crate::{
@@ -46,12 +46,9 @@ pub struct GoogleLanguageModelProvider {
pub struct State {
api_key: Option<String>,
api_key_from_env: bool,
_subscription: Subscription,
}
const GOOGLE_AI_API_KEY_VAR: &'static str = "GOOGLE_AI_API_KEY";
impl State {
fn is_authenticated(&self) -> bool {
self.api_key.is_some()
@@ -64,7 +61,6 @@ impl State {
delete_credentials.await.ok();
this.update(&mut cx, |this, cx| {
this.api_key = None;
this.api_key_from_env = false;
cx.notify();
})
})
@@ -94,20 +90,18 @@ impl State {
.clone();
cx.spawn(|this, mut cx| async move {
let (api_key, from_env) = if let Ok(api_key) = std::env::var(GOOGLE_AI_API_KEY_VAR)
{
(api_key, true)
let api_key = if let Ok(api_key) = std::env::var("GOOGLE_AI_API_KEY") {
api_key
} else {
let (_, api_key) = cx
.update(|cx| cx.read_credentials(&api_url))?
.await?
.ok_or_else(|| anyhow!("credentials not found"))?;
(String::from_utf8(api_key)?, false)
String::from_utf8(api_key)?
};
this.update(&mut cx, |this, cx| {
this.api_key = Some(api_key);
this.api_key_from_env = from_env;
cx.notify();
})
})
@@ -119,7 +113,6 @@ impl GoogleLanguageModelProvider {
pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Self {
let state = cx.new_model(|cx| State {
api_key: None,
api_key_from_env: false,
_subscription: cx.observe_global::<SettingsStore>(|_, cx| {
cx.notify();
}),
@@ -429,8 +422,6 @@ impl Render for ConfigurationView {
"Paste your Google AI API key below and hit enter to use the assistant:",
];
let env_var_set = self.state.read(cx).api_key_from_env;
if self.load_credentials_task.is_some() {
div().child(Label::new("Loading credentials...")).into_any()
} else if self.should_render_editor(cx) {
@@ -452,7 +443,7 @@ impl Render for ConfigurationView {
)
.child(
Label::new(
format!("You can also assign the {GOOGLE_AI_API_KEY_VAR} environment variable and restart Zed."),
"You can also assign the GOOGLE_AI_API_KEY environment variable and restart Zed.",
)
.size(LabelSize::Small),
)
@@ -465,21 +456,13 @@ impl Render for ConfigurationView {
h_flex()
.gap_1()
.child(Icon::new(IconName::Check).color(Color::Success))
.child(Label::new(if env_var_set {
format!("API key set in {GOOGLE_AI_API_KEY_VAR} environment variable.")
} else {
"API key configured.".to_string()
})),
.child(Label::new("API key configured.")),
)
.child(
Button::new("reset-key", "Reset key")
.icon(Some(IconName::Trash))
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.disabled(env_var_set)
.when(env_var_set, |this| {
this.tooltip(|cx| Tooltip::text(format!("To reset your API key, unset the {GOOGLE_AI_API_KEY_VAR} environment variable."), cx))
})
.on_click(cx.listener(|this, _, cx| this.reset_api_key(cx))),
)
.into_any()

View File

@@ -16,7 +16,7 @@ use settings::{Settings, SettingsStore};
use std::{sync::Arc, time::Duration};
use strum::IntoEnumIterator;
use theme::ThemeSettings;
use ui::{prelude::*, Icon, IconName, Tooltip};
use ui::{prelude::*, Icon, IconName};
use util::ResultExt;
use crate::{
@@ -40,7 +40,6 @@ pub struct OpenAiSettings {
pub struct AvailableModel {
pub name: String,
pub max_tokens: usize,
pub max_output_tokens: Option<u32>,
}
pub struct OpenAiLanguageModelProvider {
@@ -50,12 +49,9 @@ pub struct OpenAiLanguageModelProvider {
pub struct State {
api_key: Option<String>,
api_key_from_env: bool,
_subscription: Subscription,
}
const OPENAI_API_KEY_VAR: &'static str = "OPENAI_API_KEY";
impl State {
fn is_authenticated(&self) -> bool {
self.api_key.is_some()
@@ -68,7 +64,6 @@ impl State {
delete_credentials.await.log_err();
this.update(&mut cx, |this, cx| {
this.api_key = None;
this.api_key_from_env = false;
cx.notify();
})
})
@@ -97,18 +92,17 @@ impl State {
.api_url
.clone();
cx.spawn(|this, mut cx| async move {
let (api_key, from_env) = if let Ok(api_key) = std::env::var(OPENAI_API_KEY_VAR) {
(api_key, true)
let api_key = if let Ok(api_key) = std::env::var("OPENAI_API_KEY") {
api_key
} else {
let (_, api_key) = cx
.update(|cx| cx.read_credentials(&api_url))?
.await?
.ok_or_else(|| anyhow!("credentials not found"))?;
(String::from_utf8(api_key)?, false)
String::from_utf8(api_key)?
};
this.update(&mut cx, |this, cx| {
this.api_key = Some(api_key);
this.api_key_from_env = from_env;
cx.notify();
})
})
@@ -120,7 +114,6 @@ impl OpenAiLanguageModelProvider {
pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Self {
let state = cx.new_model(|cx| State {
api_key: None,
api_key_from_env: false,
_subscription: cx.observe_global::<SettingsStore>(|_this: &mut State, cx| {
cx.notify();
}),
@@ -171,7 +164,6 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider {
open_ai::Model::Custom {
name: model.name.clone(),
max_tokens: model.max_tokens,
max_output_tokens: model.max_output_tokens,
},
);
}
@@ -277,10 +269,6 @@ impl LanguageModel for OpenAiLanguageModel {
self.model.max_token_count()
}
fn max_output_tokens(&self) -> Option<u32> {
self.model.max_output_tokens()
}
fn count_tokens(
&self,
request: LanguageModelRequest,
@@ -294,7 +282,7 @@ impl LanguageModel for OpenAiLanguageModel {
request: LanguageModelRequest,
cx: &AsyncAppContext,
) -> BoxFuture<'static, Result<futures::stream::BoxStream<'static, Result<String>>>> {
let request = request.into_open_ai(self.model.id().into(), self.max_output_tokens());
let request = request.into_open_ai(self.model.id().into());
let completions = self.stream_completion(request, cx);
async move { Ok(open_ai::extract_text_from_events(completions.await?).boxed()) }.boxed()
}
@@ -307,7 +295,7 @@ impl LanguageModel for OpenAiLanguageModel {
schema: serde_json::Value,
cx: &AsyncAppContext,
) -> BoxFuture<'static, Result<futures::stream::BoxStream<'static, Result<String>>>> {
let mut request = request.into_open_ai(self.model.id().into(), self.max_output_tokens());
let mut request = request.into_open_ai(self.model.id().into());
request.tool_choice = Some(ToolChoice::Other(ToolDefinition::Function {
function: FunctionDefinition {
name: tool_name.clone(),
@@ -488,8 +476,6 @@ impl Render for ConfigurationView {
"Paste your OpenAI API key below and hit enter to use the assistant:",
];
let env_var_set = self.state.read(cx).api_key_from_env;
if self.load_credentials_task.is_some() {
div().child(Label::new("Loading credentials...")).into_any()
} else if self.should_render_editor(cx) {
@@ -511,7 +497,7 @@ impl Render for ConfigurationView {
)
.child(
Label::new(
format!("You can also assign the {OPENAI_API_KEY_VAR} environment variable and restart Zed."),
"You can also assign the OPENAI_API_KEY environment variable and restart Zed.",
)
.size(LabelSize::Small),
)
@@ -524,21 +510,13 @@ impl Render for ConfigurationView {
h_flex()
.gap_1()
.child(Icon::new(IconName::Check).color(Color::Success))
.child(Label::new(if env_var_set {
format!("API key set in {OPENAI_API_KEY_VAR} environment variable.")
} else {
"API key configured.".to_string()
})),
.child(Label::new("API key configured.")),
)
.child(
Button::new("reset-key", "Reset key")
.icon(Some(IconName::Trash))
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.disabled(env_var_set)
.when(env_var_set, |this| {
this.tooltip(|cx| Tooltip::text(format!("To reset your API key, unset the {OPENAI_API_KEY_VAR} environment variable."), cx))
})
.on_click(cx.listener(|this, _, cx| this.reset_api_key(cx))),
)
.into_any()

View File

@@ -229,7 +229,7 @@ pub struct LanguageModelRequest {
}
impl LanguageModelRequest {
pub fn into_open_ai(self, model: String, max_output_tokens: Option<u32>) -> open_ai::Request {
pub fn into_open_ai(self, model: String) -> open_ai::Request {
open_ai::Request {
model,
messages: self
@@ -251,7 +251,7 @@ impl LanguageModelRequest {
stream: true,
stop: self.stop,
temperature: self.temperature,
max_tokens: max_output_tokens,
max_tokens: None,
tools: Vec::new(),
tool_choice: None,
}

View File

@@ -94,14 +94,12 @@ impl AnthropicSettingsContent {
.filter_map(|model| match model {
anthropic::Model::Custom {
name,
display_name,
max_tokens,
tool_override,
cache_configuration,
max_output_tokens,
} => Some(provider::anthropic::AvailableModel {
name,
display_name,
max_tokens,
tool_override,
cache_configuration: cache_configuration.as_ref().map(
@@ -172,15 +170,9 @@ impl OpenAiSettingsContent {
models
.into_iter()
.filter_map(|model| match model {
open_ai::Model::Custom {
name,
max_tokens,
max_output_tokens,
} => Some(provider::open_ai::AvailableModel {
name,
max_tokens,
max_output_tokens,
}),
open_ai::Model::Custom { name, max_tokens } => {
Some(provider::open_ai::AvailableModel { name, max_tokens })
}
_ => None,
})
.collect()

View File

@@ -22,6 +22,7 @@ futures.workspace = true
gpui.workspace = true
http_client.workspace = true
language.workspace = true
lazy_static.workspace = true
log.workspace = true
lsp.workspace = true
node_runtime.workspace = true

View File

@@ -4,6 +4,7 @@ use futures::StreamExt;
use gpui::{AppContext, AsyncAppContext, Task};
use http_client::github::latest_github_release;
pub use language::*;
use lazy_static::lazy_static;
use lsp::LanguageServerBinary;
use project::project_settings::{BinarySettings, ProjectSettings};
use regex::Regex;
@@ -19,7 +20,7 @@ use std::{
str,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc, LazyLock,
Arc,
},
};
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
@@ -36,12 +37,12 @@ impl GoLspAdapter {
const SERVER_NAME: &'static str = "gopls";
}
static GOPLS_VERSION_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\d+\.\d+\.\d+").expect("Failed to create GOPLS_VERSION_REGEX"));
static GO_ESCAPE_SUBTEST_NAME_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r#"[.*+?^${}()|\[\]\\]"#).expect("Failed to create GO_ESCAPE_SUBTEST_NAME_REGEX")
});
lazy_static! {
static ref GOPLS_VERSION_REGEX: Regex = Regex::new(r"\d+\.\d+\.\d+").unwrap();
static ref GO_EXTRACT_SUBTEST_NAME_REGEX: Regex =
Regex::new(r#".*t\.Run\("([^"]*)".*"#).unwrap();
static ref GO_ESCAPE_SUBTEST_NAME_REGEX: Regex = Regex::new(r#"[.*+?^${}()|\[\]\\]"#).unwrap();
}
#[async_trait(?Send)]
impl super::LspAdapter for GoLspAdapter {

View File

@@ -6,6 +6,7 @@ use gpui::{AppContext, AsyncAppContext};
use http_client::github::{latest_github_release, GitHubLspBinaryVersion};
pub use language::*;
use language_settings::all_language_settings;
use lazy_static::lazy_static;
use lsp::LanguageServerBinary;
use project::project_settings::{BinarySettings, ProjectSettings};
use regex::Regex;
@@ -17,7 +18,6 @@ use std::{
env::consts,
path::{Path, PathBuf},
sync::Arc,
sync::LazyLock,
};
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
use util::{fs::remove_matching, maybe, ResultExt};
@@ -178,8 +178,9 @@ impl LspAdapter for RustLspAdapter {
}
fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
static REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?m)`([^`]+)\n`$").expect("Failed to create REGEX"));
lazy_static! {
static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`$").unwrap();
}
for diagnostic in &mut params.diagnostics {
for message in diagnostic
@@ -242,8 +243,9 @@ impl LspAdapter for RustLspAdapter {
Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD)
if detail.is_some() =>
{
static REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new("\\(…?\\)").unwrap());
lazy_static! {
static ref REGEX: Regex = Regex::new("\\(…?\\)").unwrap();
}
let detail = detail.unwrap();
const FUNCTION_PREFIXES: [&'static str; 6] = [
"async fn",

View File

@@ -96,8 +96,8 @@ pub fn parse_links_only(text: &str) -> Vec<(Range<usize>, MarkdownEvent)> {
start: 0,
end: text.len(),
};
for link in finder.links(&text) {
let link_range = link.start()..link.end();
for link in finder.links(&text[text_range.clone()]) {
let link_range = text_range.start + link.start()..text_range.start + link.end();
if link_range.start > text_range.start {
events.push((text_range.start..link_range.start, MarkdownEvent::Text));
@@ -118,9 +118,7 @@ pub fn parse_links_only(text: &str) -> Vec<(Range<usize>, MarkdownEvent)> {
text_range.start = link_range.end;
}
if text_range.end > text_range.start {
events.push((text_range, MarkdownEvent::Text));
}
events.push((text_range, MarkdownEvent::Text));
events
}

View File

@@ -106,7 +106,6 @@ pub enum Event {
Saved,
FileHandleChanged,
Closed,
Discarded,
DirtyChanged,
DiagnosticsUpdated,
}
@@ -1692,7 +1691,6 @@ impl MultiBuffer {
language::Event::Reparsed => Event::Reparsed(buffer.read(cx).remote_id()),
language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
language::Event::Closed => Event::Closed,
language::Event::Discarded => Event::Discarded,
language::Event::CapabilityChanged => {
self.capability = buffer.read(cx).capability();
Event::CapabilityChanged

Some files were not shown because too many files have changed in this diff Show More