Compare commits
3 Commits
explore-sw
...
workflow-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9404251dd0 | ||
|
|
289ba8fb34 | ||
|
|
ba7e894a6e |
@@ -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) {
|
||||
|
||||
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
3
.github/workflows/release_nightly.yml
vendored
3
.github/workflows/release_nightly.yml
vendored
@@ -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
57
Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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}}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
26
compose.yml
26
compose.yml
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -926,7 +926,6 @@ impl PromptLibrary {
|
||||
color: Some(cx.theme().status().predictive),
|
||||
..HighlightStyle::default()
|
||||
},
|
||||
..EditorStyle::default()
|
||||
},
|
||||
)),
|
||||
),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)?;
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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())?;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user