Compare commits

...

33 Commits

Author SHA1 Message Date
Thomas Mickley-Doyle
df0f37f247 Update handling fetch from actions 2025-04-17 07:12:50 -05:00
Thomas Mickley-Doyle
2b49347e7a Run all tests 2025-04-16 22:59:39 -05:00
Thomas Mickley-Doyle
40bf0b9338 git fetch update 2025-04-16 22:19:13 -05:00
Thomas Mickley-Doyle
245a5e4c19 Create run dir 2025-04-16 21:01:06 -05:00
Thomas Mickley-Doyle
70b757229f Create run dir 2025-04-16 20:46:09 -05:00
Thomas Mickley-Doyle
5d927cf551 Create directories 2025-04-16 16:34:44 -05:00
Thomas Mickley-Doyle
ec586d6267 Add one example to test 2025-04-16 16:18:58 -05:00
Thomas Mickley-Doyle
90d7a1a2d8 Add Anthropic key 2025-04-16 15:50:43 -05:00
Thomas Mickley-Doyle
a20e9ade0d add more depends 2025-04-16 14:49:16 -05:00
Thomas Mickley-Doyle
1a348c19f1 Add x11 and alsa 2025-04-16 14:33:10 -05:00
Thomas Mickley-Doyle
61ba414b27 fix errors 2025-04-16 14:17:20 -05:00
Thomas Mickley-Doyle
3a58360037 agent: Add git commit ID to the eval telemetry data (#28895)
Release Notes:

- N/A
2025-04-16 14:17:20 -05:00
Marshall Bowers
484a8aee54 collab: Add plan column to subscription_usages (#28889)
This PR adds a `plan` column to the `subscription_usages` table.

These tables don't have any records in them yet, so it's fine to make
the column required without a default.

Release Notes:

- N/A
2025-04-16 14:17:20 -05:00
Conrad Irwin
d21a23f536 Fix anchor_in_excerpt on replaced excerpts (#28880)
Release Notes:

- N/A
2025-04-16 14:17:20 -05:00
Bennet Bo Fenner
fb56e6f393 agent: Allow quoting selection when text thread is active (#28887)
This makes the `assistant: Quote selection` work again for text threads.
Next up is supporting this also in normal threads.

Release Notes:

- agent: Add support for inserting selections (assistant: Quote
selection) into text threads
2025-04-16 14:17:20 -05:00
Mikayla Maki
46dc85dc9e Remove bottom dock layout button (#28876)
Release Notes:

- Preview: Removed the layout button from the title bar. The
`bottom_dock_layout` setting still functions.
- Added a setting, `bottom_dock_layout`, for controlling the
relationship between the bottom dock and the left and right docks.
2025-04-16 14:17:20 -05:00
Marshall Bowers
2eaddd4e47 proto: Add ZedProTrial to Plan (#28885)
This PR adds the `ZedProTrial` member to the `Plan` enum.

Release Notes:

- N/A
2025-04-16 14:17:20 -05:00
Bennet Bo Fenner
89eadaa806 agent: Improve fuzzy matching for @mentions (#28883)
Make fuzzy search in @-mention match paths and context kinds as well
(e.g., typing "sym" should let me select the "Symbols" label, as opposed
to just paths)

Release Notes:

- agent: Improve fuzzy-matching when using @mentions
2025-04-16 14:17:20 -05:00
Anthony Eid
8dd7acb6a4 debugger: Remove or move breakpoints on file deletion/rename (#28882)
Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
2025-04-16 14:17:20 -05:00
Danilo Leal
0ead83a885 agent: Add item to open Prompt Library in the panel's menu (#28877)
Release Notes:

- agent: Added a menu item to open the Prompt Library from the panel's
dropdown menu on the top right.
2025-04-16 14:17:20 -05:00
Bennet Bo Fenner
693a2241bb agent: Add websearch tool (#28621)
Staff only for now. We'll work on making this usable for non zed.dev
users later

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-04-16 14:17:20 -05:00
5brian
56a8dd12fe vim: Fix LineUp (#27754)
Closes #27423

Release Notes:

- vim: Fixed cursor scrolling off screen with `ctrl-y`.

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-04-16 14:17:20 -05:00
Marshall Bowers
7281002c2e collab: Remove has_predict_edits_feature_flag from LlmTokenClaims (#28879)
This PR removes the `has_predict_edits_feature_flag` field from the
`LlmTokenClaims`.

We are no longer reading this anywhere.

Release Notes:

- N/A
2025-04-16 14:17:20 -05:00
Piotr Osiewicz
45003fd265 toolchain: Respect currently focused file when querying toolchains (#28875)
Closes #21743


https://github.com/user-attachments/assets/0230f233-58a4-494c-90af-28ce82f9fc1d


Release Notes:

- Virtual environment picker now looks up virtual environment based on
parent directory of active file; this enables having multiple active
virtual environments in a single worktree.
2025-04-16 14:17:20 -05:00
Danilo Leal
0a87d7499e agent: Add small design tweaks (#28874)
Some small adjustments to simplify the agent panel's design.

Release Notes:

- N/A
2025-04-16 14:17:20 -05:00
Kirill Bulatov
b591c86305 Fix more panics when removing excerpts (#28836)
Release Notes:

- Fixed a panic when an excerpt removed has an edit suggestion inlay in
it

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-04-16 14:17:20 -05:00
Smit Barmase
bccddbaaf5 editor: Expand selection to word under cursor before expanding to next enclosing syntax node (#28864)
Closes #27995

For strings in any language and Markdown, `select_larger_syntax_node`
will first select the word and then expand from there if:
- The cursor is on the word.
- The selection is inside the word.

It will not select the word and will directly proceed to expand if:
- The word is already selected.
- Multiple partial words are selected.

Todo:
- [x] Tests

Release Notes:

- Fixed `select_larger_syntax_node` to first expand to the word within a
string, and then to the larger syntax node.
2025-04-16 14:17:20 -05:00
Marshall Bowers
0c52f5384d agent: Show an error when the model requests limit has been reached (#28868)
This PR adds an error message when the model requests limit has been
hit.

Release Notes:

- N/A

Co-authored-by: Oleksiy Syvokon <oleksiy.syvokon@gmail.com>
2025-04-16 14:17:20 -05:00
Mikayla Maki
28375376dd Adjust dependencies 2025-04-16 11:52:07 -07:00
Thomas Mickley-Doyle
9c8c65b487 add libx11 2025-04-16 13:28:20 -05:00
Thomas Mickley-Doyle
07d659157e Add alsa library 2025-04-16 12:49:04 -05:00
Thomas Mickley-Doyle
1c6d79e836 Add x11 library 2025-04-16 09:39:30 -05:00
Thomas Mickley-Doyle
ee7fef2d02 Add mold linker to Eval Runs 2025-04-16 09:30:11 -05:00
61 changed files with 1512 additions and 282 deletions

View File

@@ -20,6 +20,7 @@ platforms = [
[traversal-excludes]
workspace-members = [
"remote_server",
"eval",
]
third-party = [
{ name = "reqwest", version = "0.11.27" },

View File

@@ -325,7 +325,7 @@ jobs:
cache-provider: "buildjet"
- name: Install Clang & Mold
run: ./script/remote-server && ./script/install-mold 2.34.0
run: ./script/headless && ./script/install-mold 2.34.0
- name: Configure CI
run: |

View File

@@ -21,8 +21,26 @@ jobs:
with:
clean: false
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev libxcb-shape0-dev libxcb-xfixes0-dev libxcb1-dev libxcb-render0-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-keysyms1-dev libxext-dev libxrender-dev libxrandr-dev libxtst-dev libxfixes-dev pkg-config libasound2-dev libx11-xcb-dev libxkbcommon-dev libxkbcommon-x11-dev
./script/headless && ./script/install-mold 2.34.0
- name: Create required directories
run: |
mkdir -p /home/runner/.config/zed
touch /home/runner/.config/zed/settings.json
mkdir -p ./crates/eval/repos
mkdir -p ./crates/eval/worktrees
mkdir -p ./crates/eval/runs
- name: Set up Anthropic API key
run: echo "ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }}" >> $GITHUB_ENV
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Run cargo eval
run: cargo run -p eval
run: cargo run -p eval -- --cohort_id daily-run-${{ github.run_id }}

114
Cargo.lock generated
View File

@@ -324,7 +324,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum",
"strum 0.26.3",
"thiserror 2.0.12",
"workspace-hack",
]
@@ -567,7 +567,7 @@ dependencies = [
"settings",
"smallvec",
"smol",
"strum",
"strum 0.26.3",
"telemetry_events",
"text",
"theme",
@@ -704,6 +704,7 @@ dependencies = [
"assistant_tool",
"chrono",
"collections",
"feature_flags",
"futures 0.3.31",
"gpui",
"html_to_markdown",
@@ -721,9 +722,11 @@ dependencies = [
"ui",
"unindent",
"util",
"web_search",
"workspace",
"workspace-hack",
"worktree",
"zed_llm_client",
]
[[package]]
@@ -1881,7 +1884,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum",
"strum 0.26.3",
"thiserror 2.0.12",
"tokio",
"workspace-hack",
@@ -3028,7 +3031,7 @@ dependencies = [
"settings",
"sha2",
"sqlx",
"strum",
"strum 0.26.3",
"subtle",
"supermaven_api",
"telemetry_events",
@@ -3360,7 +3363,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
"strum",
"strum 0.26.3",
"task",
"theme",
"ui",
@@ -4477,7 +4480,7 @@ dependencies = [
"optfield",
"proc-macro2",
"quote",
"strum",
"strum 0.26.3",
"syn 2.0.100",
]
@@ -4913,7 +4916,6 @@ dependencies = [
"unindent",
"util",
"uuid",
"workspace-hack",
]
[[package]]
@@ -5122,7 +5124,7 @@ dependencies = [
"serde",
"settings",
"smallvec",
"strum",
"strum 0.26.3",
"telemetry",
"theme",
"ui",
@@ -5973,7 +5975,7 @@ dependencies = [
"serde_derive",
"serde_json",
"settings",
"strum",
"strum 0.26.3",
"telemetry",
"theme",
"time",
@@ -6066,7 +6068,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum",
"strum 0.26.3",
"workspace-hack",
]
@@ -6172,7 +6174,7 @@ dependencies = [
"slotmap",
"smallvec",
"smol",
"strum",
"strum 0.26.3",
"sum_tree",
"taffy",
"thiserror 2.0.12",
@@ -6820,7 +6822,7 @@ name = "icons"
version = "0.1.0"
dependencies = [
"serde",
"strum",
"strum 0.26.3",
"workspace-hack",
]
@@ -7088,7 +7090,7 @@ dependencies = [
"paths",
"pretty_assertions",
"serde",
"strum",
"strum 0.26.3",
"util",
"workspace-hack",
]
@@ -7674,7 +7676,7 @@ dependencies = [
"serde",
"serde_json",
"smol",
"strum",
"strum 0.26.3",
"telemetry_events",
"thiserror 2.0.12",
"util",
@@ -7734,7 +7736,7 @@ dependencies = [
"serde_json",
"settings",
"smol",
"strum",
"strum 0.26.3",
"theme",
"thiserror 2.0.12",
"tiktoken-rs",
@@ -7742,6 +7744,7 @@ dependencies = [
"ui",
"util",
"workspace-hack",
"zed_llm_client",
]
[[package]]
@@ -8706,7 +8709,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum",
"strum 0.26.3",
"workspace-hack",
]
@@ -9553,7 +9556,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum",
"strum 0.26.3",
"workspace-hack",
]
@@ -12132,7 +12135,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"strum",
"strum 0.26.3",
"tracing",
"util",
"workspace-hack",
@@ -12660,7 +12663,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
"strum",
"strum 0.26.3",
"thiserror 2.0.12",
"time",
"tracing",
@@ -13705,7 +13708,7 @@ dependencies = [
"settings",
"simplelog",
"story",
"strum",
"strum 0.26.3",
"theme",
"title_bar",
"ui",
@@ -13787,7 +13790,16 @@ version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [
"strum_macros",
"strum_macros 0.26.4",
]
[[package]]
name = "strum"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
dependencies = [
"strum_macros 0.27.1",
]
[[package]]
@@ -13803,6 +13815,19 @@ dependencies = [
"syn 2.0.100",
]
[[package]]
name = "strum_macros"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.100",
]
[[package]]
name = "subtle"
version = "2.6.1"
@@ -14418,7 +14443,7 @@ dependencies = [
"serde_json_lenient",
"serde_repr",
"settings",
"strum",
"strum 0.26.3",
"thiserror 2.0.12",
"util",
"uuid",
@@ -14452,7 +14477,7 @@ dependencies = [
"serde_json",
"serde_json_lenient",
"simplelog",
"strum",
"strum 0.26.3",
"theme",
"vscode_theme",
"workspace-hack",
@@ -15453,7 +15478,7 @@ dependencies = [
"settings",
"smallvec",
"story",
"strum",
"strum 0.26.3",
"theme",
"ui_macros",
"util",
@@ -16586,6 +16611,36 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "web_search"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"gpui",
"serde",
"workspace-hack",
"zed_llm_client",
]
[[package]]
name = "web_search_providers"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"feature_flags",
"futures 0.3.31",
"gpui",
"http_client",
"language_model",
"serde",
"serde_json",
"web_search",
"workspace-hack",
"zed_llm_client",
]
[[package]]
name = "webpki-root-certs"
version = "0.26.8"
@@ -17624,7 +17679,7 @@ dependencies = [
"settings",
"smallvec",
"sqlez",
"strum",
"strum 0.26.3",
"task",
"telemetry",
"tempfile",
@@ -17769,7 +17824,7 @@ dependencies = [
"sqlx-macros-core",
"sqlx-postgres",
"sqlx-sqlite",
"strum",
"strum 0.26.3",
"subtle",
"syn 1.0.109",
"syn 2.0.100",
@@ -18264,6 +18319,8 @@ dependencies = [
"uuid",
"vim",
"vim_mode_setting",
"web_search",
"web_search_providers",
"welcome",
"windows 0.61.1",
"winresource",
@@ -18328,12 +18385,13 @@ dependencies = [
[[package]]
name = "zed_llm_client"
version = "0.4.1"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bf21350eced858d129840589158a8f6895c4fa4327ae56dd8c7d6a98495bed4"
checksum = "57a5e1b5b3ace3fb55292a4c14036723bb8a01fac4aeaa3c2b63b51228412f94"
dependencies = [
"serde",
"serde_json",
"strum 0.27.1",
"uuid",
]

View File

@@ -165,6 +165,8 @@ members = [
"crates/util_macros",
"crates/vim",
"crates/vim_mode_setting",
"crates/web_search",
"crates/web_search_providers",
"crates/welcome",
"crates/workspace",
"crates/worktree",
@@ -370,6 +372,8 @@ util = { path = "crates/util" }
util_macros = { path = "crates/util_macros" }
vim = { path = "crates/vim" }
vim_mode_setting = { path = "crates/vim_mode_setting" }
web_search = { path = "crates/web_search" }
web_search_providers = { path = "crates/web_search_providers" }
welcome = { path = "crates/welcome" }
workspace = { path = "crates/workspace" }
worktree = { path = "crates/worktree" }
@@ -601,7 +605,7 @@ wasmtime-wasi = "29"
which = "6.0.0"
wit-component = "0.221"
workspace-hack = "0.1.0"
zed_llm_client = "0.4"
zed_llm_client = "0.5.0"
zstd = "0.11"
metal = "0.29"

View File

@@ -11,7 +11,7 @@ ENV CARGO_TERM_COLOR=always
COPY script/install-mold script/
RUN ./script/install-mold "2.34.0"
COPY script/remote-server script/
RUN ./script/remote-server
COPY script/headless script/
RUN ./script/headless
COPY . .

View File

@@ -630,6 +630,7 @@
"ctrl-alt-n": "agent::NewTextThread",
"ctrl-shift-h": "agent::OpenHistory",
"ctrl-alt-c": "agent::OpenConfiguration",
"ctrl-alt-p": "assistant::OpenPromptLibrary",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-alt-/": "assistant::ToggleModelSelector",
"ctrl-shift-a": "agent::ToggleContextPicker",

View File

@@ -286,6 +286,7 @@
"cmd-alt-n": "agent::NewTextThread",
"cmd-shift-h": "agent::OpenHistory",
"cmd-alt-c": "agent::OpenConfiguration",
"cmd-alt-p": "assistant::OpenPromptLibrary",
"cmd-i": "agent::ToggleProfileSelector",
"cmd-alt-/": "assistant::ToggleModelSelector",
"cmd-shift-a": "agent::ToggleContextPicker",

View File

@@ -652,7 +652,8 @@
"path_search": true,
"read_file": true,
"regex_search": true,
"thinking": true
"thinking": true,
"web_search": true
}
},
"write": {
@@ -678,7 +679,8 @@
"regex_search": true,
"rename": true,
"symbol_info": true,
"thinking": true
"thinking": true,
"web_search": true
}
}
},

View File

@@ -5,11 +5,12 @@ use crate::thread::{
ThreadEvent, ThreadFeedback,
};
use crate::thread_store::{RulesLoadingError, ThreadStore};
use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus};
use crate::tool_use::{PendingToolUseStatus, ToolUse};
use crate::ui::{AddedContext, AgentNotification, AgentNotificationEvent, ContextPill};
use crate::{AssistantPanel, OpenActiveThreadAsMarkdown};
use anyhow::Context as _;
use assistant_settings::{AssistantSettings, NotifyWhenAgentWaiting};
use assistant_tool::ToolUseStatus;
use collections::{HashMap, HashSet};
use editor::scroll::Autoscroll;
use editor::{Editor, EditorElement, EditorStyle, MultiBuffer};
@@ -943,8 +944,8 @@ impl ActiveThread {
&tool_use.input,
self.thread
.read(cx)
.tool_result(&tool_use.id)
.map(|result| result.content.clone().into())
.output_for_tool(&tool_use.id)
.map(|output| output.clone().into())
.unwrap_or("".into()),
cx,
);
@@ -2279,12 +2280,15 @@ impl ActiveThread {
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement + use<> {
if let Some(card) = self.thread.read(cx).card_for_tool(&tool_use.id) {
return card.render(&tool_use.status, window, cx);
}
let is_open = self
.expanded_tool_uses
.get(&tool_use.id)
.copied()
.unwrap_or_default();
let is_status_finished = matches!(&tool_use.status, ToolUseStatus::Finished(_));
let fs = self
@@ -2343,6 +2347,9 @@ impl ActiveThread {
rendered.input.clone(),
tool_use_markdown_style(window, cx),
)
.code_block_renderer(markdown::CodeBlockRenderer::Default {
copy_button: false,
})
.on_url_click({
let workspace = self.workspace.clone();
move |text, window, cx| {
@@ -2369,12 +2376,16 @@ impl ActiveThread {
rendered.output.clone(),
tool_use_markdown_style(window, cx),
)
.code_block_renderer(markdown::CodeBlockRenderer::Default {
copy_button: false,
})
.on_url_click({
let workspace = self.workspace.clone();
move |text, window, cx| {
open_markdown_link(text, workspace.clone(), window, cx);
}
})
.into_any_element()
}),
)),
),
@@ -2431,6 +2442,7 @@ impl ActiveThread {
open_markdown_link(text, workspace.clone(), window, cx);
}
})
.into_any_element()
})),
),
),
@@ -2544,7 +2556,7 @@ impl ActiveThread {
)
} else {
v_flex()
.my_3()
.my_2()
.rounded_lg()
.border_1()
.border_color(self.tool_card_border_color(cx))
@@ -2761,7 +2773,7 @@ impl ActiveThread {
)
})
}
})
}).into_any_element()
}
fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {

View File

@@ -25,6 +25,7 @@ use language_model_selector::ToggleModelSelector;
use project::Project;
use prompt_library::{PromptLibrary, open_prompt_library};
use prompt_store::PromptBuilder;
use proto::Plan;
use settings::{Settings, update_settings_file};
use time::UtcOffset;
use ui::{
@@ -1112,16 +1113,16 @@ impl AssistantPanel {
"New Text Thread",
NewTextThread.boxed_clone(),
)
.action("Settings", OpenConfiguration.boxed_clone())
.action("Prompt Library", Box::new(OpenPromptLibrary))
.action("Settings", Box::new(OpenConfiguration))
.separator()
.action(
"Install MCPs",
zed_actions::Extensions {
Box::new(zed_actions::Extensions {
category_filter: Some(
zed_actions::ExtensionCategoryFilter::ContextServers,
),
}
.boxed_clone(),
}),
)
},
))
@@ -1449,6 +1450,9 @@ impl AssistantPanel {
ThreadError::MaxMonthlySpendReached => {
self.render_max_monthly_spend_reached_error(cx)
}
ThreadError::ModelRequestLimitReached { plan } => {
self.render_model_request_limit_reached_error(plan, cx)
}
ThreadError::Message { header, message } => {
self.render_error_message(header, message, cx)
}
@@ -1551,6 +1555,71 @@ impl AssistantPanel {
.into_any()
}
fn render_model_request_limit_reached_error(
&self,
plan: Plan,
cx: &mut Context<Self>,
) -> AnyElement {
let error_message = match plan {
Plan::ZedPro => {
"Model request limit reached. Upgrade to usage-based billing for more requests."
}
Plan::ZedProTrial => {
"Model request limit reached. Upgrade to Zed Pro for more requests."
}
Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
};
let call_to_action = match plan {
Plan::ZedPro => "Upgrade to usage-based billing",
Plan::ZedProTrial => "Upgrade to Zed Pro",
Plan::Free => "Upgrade to Zed Pro",
};
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
)
.child(
div()
.id("error-message")
.max_h_24()
.overflow_y_scroll()
.child(Label::new(error_message)),
)
.child(
h_flex()
.justify_end()
.mt_1()
.child(
Button::new("subscribe", call_to_action).on_click(cx.listener(
|this, _, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
},
)),
)
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.notify();
},
))),
)
.into_any()
}
fn render_error_message(
&self,
header: SharedString,
@@ -1723,10 +1792,27 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
fn quote_selection(
&self,
_workspace: &mut Workspace,
_creases: Vec<(String, String)>,
_window: &mut Window,
_cx: &mut Context<Workspace>,
workspace: &mut Workspace,
creases: Vec<(String, String)>,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
return;
};
if !panel.focus_handle(cx).contains_focused(window, cx) {
workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
}
panel.update(cx, |_, cx| {
// Wait to create a new context until the workspace is no longer
// being updated.
cx.defer_in(window, move |panel, window, cx| {
if let Some(context) = panel.active_context_editor() {
context.update(cx, |context, cx| context.quote_creases(creases, window, cx));
};
});
});
}
}

View File

@@ -8,6 +8,7 @@ use std::sync::atomic::AtomicBool;
use anyhow::Result;
use editor::{CompletionProvider, Editor, ExcerptId};
use file_icons::FileIcons;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{App, Entity, Task, WeakEntity};
use http_client::HttpClientWithUrl;
use language::{Buffer, CodeLabel, HighlightId};
@@ -37,7 +38,24 @@ pub(crate) enum Match {
File(FileMatch),
Thread(ThreadMatch),
Fetch(SharedString),
Mode(ContextPickerMode),
Mode(ModeMatch),
}
pub struct ModeMatch {
mat: Option<StringMatch>,
mode: ContextPickerMode,
}
impl Match {
pub fn score(&self) -> f64 {
match self {
Match::File(file) => file.mat.score,
Match::Mode(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
Match::Thread(_) => 1.,
Match::Symbol(_) => 1.,
Match::Fetch(_) => 1.,
}
}
}
fn search(
@@ -126,19 +144,54 @@ fn search(
matches.extend(
supported_context_picker_modes(&thread_store)
.into_iter()
.map(Match::Mode),
.map(|mode| Match::Mode(ModeMatch { mode, mat: None })),
);
Task::ready(matches)
} else {
let executor = cx.background_executor().clone();
let search_files_task =
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
let modes = supported_context_picker_modes(&thread_store);
let mode_candidates = modes
.iter()
.enumerate()
.map(|(ix, mode)| StringMatchCandidate::new(ix, mode.mention_prefix()))
.collect::<Vec<_>>();
cx.background_spawn(async move {
search_files_task
let mut matches = search_files_task
.await
.into_iter()
.map(Match::File)
.collect()
.collect::<Vec<_>>();
let mode_matches = fuzzy::match_strings(
&mode_candidates,
&query,
false,
100,
&Arc::new(AtomicBool::default()),
executor,
)
.await;
matches.extend(mode_matches.into_iter().map(|mat| {
Match::Mode(ModeMatch {
mode: modes[mat.candidate_id],
mat: Some(mat),
})
}));
matches.sort_by(|a, b| {
b.score()
.partial_cmp(&a.score())
.unwrap_or(std::cmp::Ordering::Equal)
});
matches
})
}
}
@@ -548,7 +601,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
context_store.clone(),
http_client.clone(),
)),
Match::Mode(mode) => {
Match::Mode(ModeMatch { mode, .. }) => {
Some(Self::completion_for_mode(source_range.clone(), mode))
}
})

View File

@@ -6,7 +6,7 @@ use std::time::Instant;
use anyhow::{Context as _, Result, anyhow};
use assistant_settings::AssistantSettings;
use assistant_tool::{ActionLog, Tool, ToolWorkingSet};
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::{BTreeMap, HashMap};
use feature_flags::{self, FeatureFlagAppExt};
@@ -18,12 +18,13 @@ use language_model::{
ConfiguredModel, LanguageModel, LanguageModelCompletionEvent, LanguageModelId,
LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
Role, StopReason, TokenUsage,
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent,
ModelRequestLimitReachedError, PaymentRequiredError, Role, StopReason, TokenUsage,
};
use project::Project;
use project::git_store::{GitStore, GitStoreCheckpoint, RepositoryState};
use prompt_store::PromptBuilder;
use proto::Plan;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -630,6 +631,14 @@ impl Thread {
self.tool_use.tool_result(id)
}
pub fn output_for_tool(&self, id: &LanguageModelToolUseId) -> Option<&Arc<str>> {
Some(&self.tool_use.tool_result(id)?.content)
}
pub fn card_for_tool(&self, id: &LanguageModelToolUseId) -> Option<AnyToolCard> {
self.tool_use.tool_result_card(id).cloned()
}
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
self.tool_use.message_has_tool_results(message_id)
}
@@ -1150,6 +1159,12 @@ impl Thread {
cx.emit(ThreadEvent::ShowError(
ThreadError::MaxMonthlySpendReached,
));
} else if let Some(error) =
error.downcast_ref::<ModelRequestLimitReachedError>()
{
cx.emit(ThreadEvent::ShowError(
ThreadError::ModelRequestLimitReached { plan: error.plan },
));
} else if let Some(known_error) =
error.downcast_ref::<LanguageModelKnownError>()
{
@@ -1419,6 +1434,12 @@ impl Thread {
)
};
// Store the card separately if it exists
if let Some(card) = tool_result.card.clone() {
self.tool_use
.insert_tool_result_card(tool_use_id.clone(), card);
}
cx.spawn({
async move |thread: WeakEntity<Thread>, cx| {
let output = tool_result.output.await;
@@ -1929,6 +1950,8 @@ pub enum ThreadError {
PaymentRequired,
#[error("Max monthly spend reached")]
MaxMonthlySpendReached,
#[error("Model request limit reached")]
ModelRequestLimitReached { plan: Plan },
#[error("Message {header}: {message}")]
Message {
header: SharedString,

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
use assistant_tool::{Tool, ToolWorkingSet};
use assistant_tool::{AnyToolCard, Tool, ToolUseStatus, ToolWorkingSet};
use collections::HashMap;
use futures::FutureExt as _;
use futures::future::Shared;
@@ -27,26 +27,7 @@ pub struct ToolUse {
pub needs_confirmation: bool,
}
#[derive(Debug, Clone)]
pub enum ToolUseStatus {
NeedsConfirmation,
Pending,
Running,
Finished(SharedString),
Error(SharedString),
}
impl ToolUseStatus {
pub fn text(&self) -> SharedString {
match self {
ToolUseStatus::NeedsConfirmation => "".into(),
ToolUseStatus::Pending => "".into(),
ToolUseStatus::Running => "".into(),
ToolUseStatus::Finished(out) => out.clone(),
ToolUseStatus::Error(out) => out.clone(),
}
}
}
pub const USING_TOOL_MARKER: &str = "<using_tool>";
pub struct ToolUseState {
tools: Entity<ToolWorkingSet>,
@@ -54,10 +35,9 @@ pub struct ToolUseState {
tool_uses_by_user_message: HashMap<MessageId, Vec<LanguageModelToolUseId>>,
tool_results: HashMap<LanguageModelToolUseId, LanguageModelToolResult>,
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
tool_result_cards: HashMap<LanguageModelToolUseId, AnyToolCard>,
}
pub const USING_TOOL_MARKER: &str = "<using_tool>";
impl ToolUseState {
pub fn new(tools: Entity<ToolWorkingSet>) -> Self {
Self {
@@ -66,6 +46,7 @@ impl ToolUseState {
tool_uses_by_user_message: HashMap::default(),
tool_results: HashMap::default(),
pending_tool_uses_by_id: HashMap::default(),
tool_result_cards: HashMap::default(),
}
}
@@ -257,6 +238,18 @@ impl ToolUseState {
self.tool_results.get(tool_use_id)
}
pub fn tool_result_card(&self, tool_use_id: &LanguageModelToolUseId) -> Option<&AnyToolCard> {
self.tool_result_cards.get(tool_use_id)
}
pub fn insert_tool_result_card(
&mut self,
tool_use_id: LanguageModelToolUseId,
card: AnyToolCard,
) {
self.tool_result_cards.insert(tool_use_id, card);
}
pub fn request_tool_use(
&mut self,
assistant_message_id: MessageId,

View File

@@ -191,15 +191,12 @@ impl RenderOnce for ContextPill {
ContextPill::Suggested {
name,
icon_path: _,
kind,
kind: _,
focused,
on_click,
} => base_pill
.cursor_pointer()
.pr_1()
.when(*focused, |this| {
this.bg(color.element_background.opacity(0.5))
})
.border_dashed()
.border_color(if *focused {
color.border_focused
@@ -207,30 +204,17 @@ impl RenderOnce for ContextPill {
color.border
})
.hover(|style| style.bg(color.element_hover.opacity(0.5)))
.when(*focused, |this| {
this.bg(color.element_background.opacity(0.5))
})
.child(
div().px_0p5().max_w_64().child(
div().max_w_64().child(
Label::new(name.clone())
.size(LabelSize::Small)
.color(Color::Muted)
.truncate(),
),
)
.child(
Label::new(match kind {
ContextKind::File => "Active Tab",
ContextKind::Thread
| ContextKind::Directory
| ContextKind::FetchedUrl
| ContextKind::Symbol => "Active",
})
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.child(
Icon::new(IconName::Plus)
.size(IconSize::XSmall)
.into_any_element(),
)
.tooltip(|window, cx| {
Tooltip::with_meta("Suggested Context", None, "Click to add it", window, cx)
})

View File

@@ -9,6 +9,10 @@ use std::fmt::Formatter;
use std::sync::Arc;
use anyhow::Result;
use gpui::AnyElement;
use gpui::Context;
use gpui::IntoElement;
use gpui::Window;
use gpui::{App, Entity, SharedString, Task};
use icons::IconName;
use language_model::LanguageModelRequestMessage;
@@ -24,16 +28,87 @@ pub fn init(cx: &mut App) {
ToolRegistry::default_global(cx);
}
/// The result of running a tool
#[derive(Debug, Clone)]
pub enum ToolUseStatus {
NeedsConfirmation,
Pending,
Running,
Finished(SharedString),
Error(SharedString),
}
impl ToolUseStatus {
pub fn text(&self) -> SharedString {
match self {
ToolUseStatus::NeedsConfirmation => "".into(),
ToolUseStatus::Pending => "".into(),
ToolUseStatus::Running => "".into(),
ToolUseStatus::Finished(out) => out.clone(),
ToolUseStatus::Error(out) => out.clone(),
}
}
}
/// The result of running a tool, containing both the asynchronous output
/// and an optional card view that can be rendered immediately.
pub struct ToolResult {
/// The asynchronous task that will eventually resolve to the tool's output
pub output: Task<Result<String>>,
/// An optional view to present the output of the tool.
pub card: Option<AnyToolCard>,
}
pub trait ToolCard: 'static + Sized {
fn render(
&mut self,
status: &ToolUseStatus,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement;
}
#[derive(Clone)]
pub struct AnyToolCard {
entity: gpui::AnyEntity,
render: fn(
entity: gpui::AnyEntity,
status: &ToolUseStatus,
window: &mut Window,
cx: &mut App,
) -> AnyElement,
}
impl<T: ToolCard> From<Entity<T>> for AnyToolCard {
fn from(entity: Entity<T>) -> Self {
fn downcast_render<T: ToolCard>(
entity: gpui::AnyEntity,
status: &ToolUseStatus,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let entity = entity.downcast::<T>().unwrap();
entity.update(cx, |entity, cx| {
entity.render(status, window, cx).into_any_element()
})
}
Self {
entity: entity.into(),
render: downcast_render::<T>,
}
}
}
impl AnyToolCard {
pub fn render(&self, status: &ToolUseStatus, window: &mut Window, cx: &mut App) -> AnyElement {
(self.render)(self.entity.clone(), status, window, cx)
}
}
impl From<Task<Result<String>>> for ToolResult {
/// Convert from a task to a ToolResult
/// Convert from a task to a ToolResult with no card
fn from(output: Task<Result<String>>) -> Self {
Self { output }
Self { output, card: None }
}
}

View File

@@ -16,6 +16,7 @@ anyhow.workspace = true
assistant_tool.workspace = true
chrono.workspace = true
collections.workspace = true
feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
html_to_markdown.workspace = true
@@ -32,7 +33,9 @@ ui.workspace = true
util.workspace = true
worktree.workspace = true
open = { workspace = true }
web_search.workspace = true
workspace-hack.workspace = true
zed_llm_client.workspace = true
[dev-dependencies]
collections = { workspace = true, features = ["test-support"] }

View File

@@ -22,14 +22,17 @@ mod schema;
mod symbol_info_tool;
mod terminal_tool;
mod thinking_tool;
mod web_search_tool;
use std::sync::Arc;
use assistant_tool::ToolRegistry;
use copy_path_tool::CopyPathTool;
use feature_flags::FeatureFlagAppExt;
use gpui::App;
use http_client::HttpClientWithUrl;
use move_path_tool::MovePathTool;
use web_search_tool::WebSearchTool;
use crate::batch_tool::BatchTool;
use crate::code_action_tool::CodeActionTool;
@@ -56,28 +59,39 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
assistant_tool::init(cx);
let registry = ToolRegistry::global(cx);
registry.register_tool(TerminalTool);
registry.register_tool(BatchTool);
registry.register_tool(CreateDirectoryTool);
registry.register_tool(CreateFileTool);
registry.register_tool(CopyPathTool);
registry.register_tool(DeletePathTool);
registry.register_tool(FindReplaceFileTool);
registry.register_tool(SymbolInfoTool);
registry.register_tool(CodeActionTool);
registry.register_tool(MovePathTool);
registry.register_tool(DiagnosticsTool);
registry.register_tool(ListDirectoryTool);
registry.register_tool(NowTool);
registry.register_tool(OpenTool);
registry.register_tool(CodeSymbolsTool);
registry.register_tool(ContentsTool);
registry.register_tool(CopyPathTool);
registry.register_tool(CreateDirectoryTool);
registry.register_tool(CreateFileTool);
registry.register_tool(DeletePathTool);
registry.register_tool(DiagnosticsTool);
registry.register_tool(FetchTool::new(http_client));
registry.register_tool(FindReplaceFileTool);
registry.register_tool(ListDirectoryTool);
registry.register_tool(MovePathTool);
registry.register_tool(NowTool);
registry.register_tool(OpenTool);
registry.register_tool(PathSearchTool);
registry.register_tool(ReadFileTool);
registry.register_tool(RegexSearchTool);
registry.register_tool(RenameTool);
registry.register_tool(SymbolInfoTool);
registry.register_tool(TerminalTool);
registry.register_tool(ThinkingTool);
registry.register_tool(FetchTool::new(http_client));
cx.observe_flag::<feature_flags::ZedProWebSearchTool, _>({
move |is_enabled, cx| {
if is_enabled {
ToolRegistry::global(cx).register_tool(WebSearchTool);
} else {
ToolRegistry::global(cx).unregister_tool(WebSearchTool);
}
}
})
.detach();
}
#[cfg(test)]

View File

@@ -0,0 +1,213 @@
use std::{sync::Arc, time::Duration};
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
use futures::{FutureExt, TryFutureExt};
use gpui::{
Animation, AnimationExt, App, AppContext, Context, Entity, IntoElement, Task, Window,
pulsating_between,
};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use ui::{IconName, Tooltip, prelude::*};
use web_search::WebSearchRegistry;
use zed_llm_client::WebSearchResponse;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct WebSearchToolInput {
/// The search term or question to query on the web.
query: String,
}
pub struct WebSearchTool;
impl Tool for WebSearchTool {
fn name(&self) -> String {
"web_search".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}
fn description(&self) -> String {
"Search the web for information using your query. Use this when you need real-time information, facts, or data that might not be in your training. Results will include snippets and links from relevant web pages.".into()
}
fn icon(&self) -> IconName {
IconName::Globe
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<WebSearchToolInput>(format)
}
fn ui_text(&self, _input: &serde_json::Value) -> String {
"Web Search".to_string()
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<WebSearchToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let Some(provider) = WebSearchRegistry::read_global(cx).active_provider() else {
return Task::ready(Err(anyhow!("Web search is not available."))).into();
};
let search_task = provider.search(input.query, cx).map_err(Arc::new).shared();
let output = cx.background_spawn({
let search_task = search_task.clone();
async move {
let response = search_task.await.map_err(|err| anyhow!(err))?;
serde_json::to_string(&response).context("Failed to serialize search results")
}
});
ToolResult {
output,
card: Some(cx.new(|cx| WebSearchToolCard::new(search_task, cx)).into()),
}
}
}
struct WebSearchToolCard {
response: Option<Result<WebSearchResponse>>,
_task: Task<()>,
}
impl WebSearchToolCard {
fn new(
search_task: impl 'static + Future<Output = Result<WebSearchResponse, Arc<anyhow::Error>>>,
cx: &mut Context<Self>,
) -> Self {
let _task = cx.spawn(async move |this, cx| {
let response = search_task.await.map_err(|err| anyhow!(err));
this.update(cx, |this, cx| {
this.response = Some(response);
cx.notify();
})
.ok();
});
Self {
response: None,
_task,
}
}
}
impl ToolCard for WebSearchToolCard {
fn render(
&mut self,
_status: &ToolUseStatus,
_window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let header = h_flex()
.id("tool-label-container")
.gap_1p5()
.max_w_full()
.overflow_x_scroll()
.child(
Icon::new(IconName::Globe)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(match self.response.as_ref() {
Some(Ok(response)) => {
let text: SharedString = if response.citations.len() == 1 {
"1 result".into()
} else {
format!("{} results", response.citations.len()).into()
};
h_flex()
.gap_1p5()
.child(Label::new("Searched the Web").size(LabelSize::Small))
.child(
div()
.size(px(3.))
.rounded_full()
.bg(cx.theme().colors().text),
)
.child(Label::new(text).size(LabelSize::Small))
.into_any_element()
}
Some(Err(error)) => div()
.id("web-search-error")
.child(Label::new("Web Search failed").size(LabelSize::Small))
.tooltip(Tooltip::text(error.to_string()))
.into_any_element(),
None => Label::new("Searching the Web…")
.size(LabelSize::Small)
.with_animation(
"web-search-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.6, 1.)),
|label, delta| label.alpha(delta),
)
.into_any_element(),
})
.into_any();
let content =
self.response.as_ref().and_then(|response| match response {
Ok(response) => {
Some(
v_flex()
.ml_1p5()
.pl_1p5()
.border_l_1()
.border_color(cx.theme().colors().border_variant)
.gap_1()
.children(response.citations.iter().enumerate().map(
|(index, citation)| {
let title = citation.title.clone();
let url = citation.url.clone();
Button::new(("citation", index), title)
.label_size(LabelSize::Small)
.color(Color::Muted)
.icon(IconName::ArrowUpRight)
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::End)
.truncate(true)
.tooltip({
let url = url.clone();
move |window, cx| {
Tooltip::with_meta(
"Citation Link",
None,
url.clone(),
window,
cx,
)
}
})
.on_click({
let url = url.clone();
move |_, _, cx| cx.open_url(&url)
})
},
))
.into_any(),
)
}
Err(_) => None,
});
v_flex().my_2().gap_1().child(header).children(content)
}
}

View File

@@ -0,0 +1,4 @@
alter table subscription_usages
add column plan text not null;
create index ix_subscription_usages_on_plan on subscription_usages (plan);

View File

@@ -1,4 +1,5 @@
use crate::db::UserId;
use crate::db::billing_subscription::SubscriptionKind;
use sea_orm::entity::prelude::*;
use time::PrimitiveDateTime;
@@ -10,6 +11,7 @@ pub struct Model {
pub user_id: UserId,
pub period_start_at: PrimitiveDateTime,
pub period_end_at: PrimitiveDateTime,
pub plan: SubscriptionKind,
pub model_requests: i32,
pub edit_predictions: i32,
}

View File

@@ -25,7 +25,6 @@ pub struct LlmTokenClaims {
pub is_staff: bool,
pub has_llm_closed_beta_feature_flag: bool,
pub bypass_account_age_check: bool,
pub has_predict_edits_feature_flag: bool,
pub has_llm_subscription: bool,
pub max_monthly_spend_in_cents: u32,
pub custom_llm_monthly_allowance_in_cents: Option<u32>,
@@ -70,9 +69,6 @@ impl LlmTokenClaims {
bypass_account_age_check: feature_flags
.iter()
.any(|flag| flag == "bypass-account-age-check"),
has_predict_edits_feature_flag: feature_flags
.iter()
.any(|flag| flag == "predict-edits"),
has_llm_subscription: has_legacy_llm_subscription,
max_monthly_spend_in_cents: billing_preferences
.map_or(DEFAULT_MAX_MONTHLY_SPEND.0, |preferences| {

View File

@@ -3707,7 +3707,9 @@ async fn count_language_model_tokens(
let rate_limit: Box<dyn RateLimit> = match session.current_plan(&session.db().await).await? {
proto::Plan::ZedPro => Box::new(ZedProCountLanguageModelTokensRateLimit),
proto::Plan::Free => Box::new(FreeCountLanguageModelTokensRateLimit),
proto::Plan::Free | proto::Plan::ZedProTrial => {
Box::new(FreeCountLanguageModelTokensRateLimit)
}
};
session
@@ -3827,7 +3829,7 @@ async fn compute_embeddings(
let rate_limit: Box<dyn RateLimit> = match session.current_plan(&session.db().await).await? {
proto::Plan::ZedPro => Box::new(ZedProComputeEmbeddingsRateLimit),
proto::Plan::Free => Box::new(FreeComputeEmbeddingsRateLimit),
proto::Plan::Free | proto::Plan::ZedProTrial => Box::new(FreeComputeEmbeddingsRateLimit),
};
session

View File

@@ -49,8 +49,8 @@ use language::{
};
use lsp::DiagnosticSeverity;
use multi_buffer::{
Anchor, AnchorRangeExt, MultiBuffer, MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot,
RowInfo, ToOffset, ToPoint,
Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferPoint, MultiBufferRow,
MultiBufferSnapshot, RowInfo, ToOffset, ToPoint,
};
use serde::Deserialize;
use std::{
@@ -574,6 +574,21 @@ impl DisplayMap {
self.block_map.read(snapshot, edits);
}
pub fn remove_inlays_for_excerpts(&mut self, excerpts_removed: &[ExcerptId]) {
let to_remove = self
.inlay_map
.current_inlays()
.filter_map(|inlay| {
if excerpts_removed.contains(&inlay.position.excerpt_id) {
Some(inlay.id)
} else {
None
}
})
.collect::<Vec<_>>();
self.inlay_map.splice(&to_remove, Vec::new());
}
fn tab_size(buffer: &Entity<MultiBuffer>, cx: &App) -> NonZeroU32 {
let buffer = buffer.read(cx).as_singleton().map(|buffer| buffer.read(cx));
let language = buffer

View File

@@ -4170,10 +4170,13 @@ impl Editor {
if let Some(InlaySplice {
to_remove,
to_insert,
}) = self.inlay_hint_cache.remove_excerpts(excerpts_removed)
}) = self.inlay_hint_cache.remove_excerpts(&excerpts_removed)
{
self.splice_inlays(&to_remove, to_insert, cx);
}
self.display_map.update(cx, |display_map, _| {
display_map.remove_inlays_for_excerpts(&excerpts_removed)
});
return;
}
InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
@@ -12519,6 +12522,45 @@ impl Editor {
.iter()
.map(|selection| {
let old_range = selection.start..selection.end;
if let Some((node, _)) = buffer.syntax_ancestor(old_range.clone()) {
// manually select word at selection
if ["string_content", "inline"].contains(&node.kind()) {
let word_range = {
let display_point = buffer
.offset_to_point(old_range.start)
.to_display_point(&display_map);
let Range { start, end } =
movement::surrounding_word(&display_map, display_point);
start.to_point(&display_map).to_offset(&buffer)
..end.to_point(&display_map).to_offset(&buffer)
};
// ignore if word is already selected
if !word_range.is_empty() && old_range != word_range {
let last_word_range = {
let display_point = buffer
.offset_to_point(old_range.end)
.to_display_point(&display_map);
let Range { start, end } =
movement::surrounding_word(&display_map, display_point);
start.to_point(&display_map).to_offset(&buffer)
..end.to_point(&display_map).to_offset(&buffer)
};
// only select word if start and end point belongs to same word
if word_range == last_word_range {
selected_larger_node = true;
return Selection {
id: selection.id,
start: word_range.start,
end: word_range.end,
goal: SelectionGoal::None,
reversed: selection.reversed,
};
}
}
}
}
let mut new_range = old_range.clone();
let mut new_node = None;
while let Some((node, containing_range)) = buffer.syntax_ancestor(new_range.clone())

View File

@@ -6309,7 +6309,187 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
use mod1::mod2::«{mod3, mod4}ˇ»;
fn fn_1«ˇ(param1: bool, param2: &str)» {
«ˇlet var1 = "text";»
let var1 = "«ˇtext»";
}
"#},
cx,
);
});
}
#[gpui::test]
async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let language = Arc::new(Language::new(
LanguageConfig::default(),
Some(tree_sitter_rust::LANGUAGE.into()),
));
let text = r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "hello world";
}
"#
.unindent();
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
editor
.condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
.await;
// Test 1: Cursor on a letter of a string word
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
]);
});
});
editor.update_in(cx, |editor, window, cx| {
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "hˇello world";
}
"#},
cx,
);
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "«ˇhello» world";
}
"#},
cx,
);
});
// Test 2: Partial selection within a word
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
]);
});
});
editor.update_in(cx, |editor, window, cx| {
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "h«elˇ»lo world";
}
"#},
cx,
);
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "«ˇhello» world";
}
"#},
cx,
);
});
// Test 3: Complete word already selected
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
]);
});
});
editor.update_in(cx, |editor, window, cx| {
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "«helloˇ» world";
}
"#},
cx,
);
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "«hello worldˇ»";
}
"#},
cx,
);
});
// Test 4: Selection spanning across words
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
]);
});
});
editor.update_in(cx, |editor, window, cx| {
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "hel«lo woˇ»rld";
}
"#},
cx,
);
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
let var1 = "«ˇhello world»";
}
"#},
cx,
);
});
// Test 5: Expansion beyond string
editor.update_in(cx, |editor, window, cx| {
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
assert_text_with_selections(
editor,
indoc! {r#"
use mod1::mod2::{mod3, mod4};
fn fn_1(param1: bool, param2: &str) {
«ˇlet var1 = "hello world";»
}
"#},
cx,

View File

@@ -555,12 +555,12 @@ impl InlayHintCache {
/// Completely forget of certain excerpts that were removed from the multibuffer.
pub(super) fn remove_excerpts(
&mut self,
excerpts_removed: Vec<ExcerptId>,
excerpts_removed: &[ExcerptId],
) -> Option<InlaySplice> {
let mut to_remove = Vec::new();
for excerpt_to_remove in excerpts_removed {
self.update_tasks.remove(&excerpt_to_remove);
if let Some(cached_hints) = self.hints.remove(&excerpt_to_remove) {
self.update_tasks.remove(excerpt_to_remove);
if let Some(cached_hints) = self.hints.remove(excerpt_to_remove) {
let cached_hints = cached_hints.read();
to_remove.extend(cached_hints.ordered_hints.iter().copied());
}

View File

@@ -43,7 +43,6 @@ toml.workspace = true
unindent.workspace = true
util.workspace = true
uuid = { version = "1.6", features = ["v4"] }
workspace-hack.workspace = true
[[bin]]
name = "eval"

View File

@@ -54,6 +54,9 @@ struct Args {
/// Maximum number of examples to run concurrently.
#[arg(long, default_value = "10")]
concurrency: usize,
/// Optional cohort ID to group runs together (useful for GitHub Actions)
#[arg(long)]
cohort_id: Option<String>,
}
fn main() {
@@ -187,6 +190,7 @@ fn main() {
);
let repo_url = example.base.url.clone();
let revision = example.base.revision.clone();
if repo_urls.insert(repo_url.clone()) {
let repo_path = repo_path_for_url(&repo_url);
@@ -201,7 +205,18 @@ fn main() {
let git_task = cx.spawn(async move |_cx| {
std::fs::create_dir_all(&repo_path)?;
run_git(&repo_path, &["init"]).await?;
run_git(&repo_path, &["remote", "add", "origin", &repo_url]).await
run_git(&repo_path, &["remote", "add", "origin", &repo_url]).await?;
run_git(
&repo_path,
&[
"fetch",
"origin",
"+refs/heads/*:refs/remotes/origin/*",
],
)
.await?;
run_git(&repo_path, &["fetch", "origin", &revision])
.await
});
clone_tasks.push(git_task);
@@ -234,15 +249,17 @@ fn main() {
let judge_repetitions = args.judge_repetitions;
let concurrency = args.concurrency;
let cohort_id = args.cohort_id.clone();
let tasks = examples
.into_iter()
.map(|example| {
let app_state = app_state.clone();
let model = model.clone();
let cohort_id = cohort_id.clone();
cx.spawn(async move |cx| {
let result =
run_example(&example, model, app_state, judge_repetitions, cx).await;
run_example(&example, model, app_state, judge_repetitions, cohort_id, cx).await;
(result, example)
})
})
@@ -271,12 +288,12 @@ fn main() {
match judge_result {
Ok(judge_output) => {
const SCORES: [&str; 6] = ["💀", "😭", "😔", "😐", "🙂", "🤩"];
let score: u32 = judge_output.score;
let score_index = (score.min(5)) as usize;
println!(
"{} {}{}",
SCORES[judge_output.score.min(5) as usize],
example.log_prefix,
judge_output.score,
SCORES[score_index], example.log_prefix, judge_output.score,
);
judge_scores.push(judge_output.score);
}
@@ -304,7 +321,6 @@ fn main() {
std::thread::sleep(std::time::Duration::from_secs(2));
// Flush telemetry events before exiting
app_state.client.telemetry().flush_events();
cx.update(|cx| cx.quit())
@@ -318,6 +334,7 @@ async fn run_example(
model: Arc<dyn LanguageModel>,
app_state: Arc<AgentAppState>,
judge_repetitions: u32,
optional_cohort_id: Option<String>,
cx: &mut AsyncApp,
) -> Result<Vec<Result<JudgeOutput>>> {
let run_output = cx
@@ -330,14 +347,19 @@ async fn run_example(
for round in 0..judge_repetitions {
let judge_result = example.judge(model.clone(), diff.clone(), round, cx).await;
// Log telemetry for this judge result
if let Ok(judge_output) = &judge_result {
let cohort_id = example
.output_file_path
.parent()
.and_then(|p| p.file_name())
.map(|name| name.to_string_lossy().to_string())
.unwrap_or(chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string());
// Use the provided cohort_id if available, otherwise generate one from the output path
let cohort_id = optional_cohort_id.clone().unwrap_or_else(|| {
example
.output_file_path
.parent()
.and_then(|p| p.file_name())
.map(|name| name.to_string_lossy().to_string())
.unwrap_or(chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string())
});
let path = std::path::Path::new(".");
let commit_id = get_current_commit_id(path).await.unwrap_or_default();
telemetry::event!(
"Agent Eval Completed",
@@ -353,7 +375,8 @@ async fn run_example(
model_provider = model.provider_id().to_string(),
repository_url = example.base.url.clone(),
repository_revision = example.base.revision.clone(),
diagnostics_summary = run_output.diagnostics
diagnostics_summary = run_output.diagnostics,
commit_id = commit_id
);
}
@@ -524,3 +547,13 @@ pub fn authenticate_model_provider(
let model_provider = model_registry.provider(&provider_id).unwrap();
model_provider.authenticate(cx)
}
pub async fn get_current_commit_id(repo_path: &Path) -> Option<String> {
(run_git(repo_path, &["rev-parse", "HEAD"]).await).ok()
}
pub fn get_current_commit_id_sync(repo_path: &Path) -> String {
futures::executor::block_on(async {
get_current_commit_id(repo_path).await.unwrap_or_default()
})
}

View File

@@ -151,7 +151,7 @@ impl Example {
);
run_git(
&repo_path,
&["fetch", "--depth", "1", "origin", &self.base.revision],
&["fetch", "origin", &self.base.revision],
)
.await?;
}

View File

@@ -84,6 +84,11 @@ impl FeatureFlag for ZedPro {
const NAME: &'static str = "zed-pro";
}
pub struct ZedProWebSearchTool {}
impl FeatureFlag for ZedProWebSearchTool {
const NAME: &'static str = "zed-pro-web-search-tool";
}
pub struct NotebookFeatureFlag;
impl FeatureFlag for NotebookFeatureFlag {

View File

@@ -142,6 +142,27 @@ impl fmt::Display for MaxMonthlySpendReachedError {
}
}
#[derive(Error, Debug)]
pub struct ModelRequestLimitReachedError {
pub plan: Plan,
}
impl fmt::Display for ModelRequestLimitReachedError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let message = match self.plan {
Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
Plan::ZedPro => {
"Model request limit reached. Upgrade to usage-based billing for more requests."
}
Plan::ZedProTrial => {
"Model request limit reached. Upgrade to Zed Pro for more requests."
}
};
write!(f, "{message}")
}
}
#[derive(Clone, Default)]
pub struct LlmApiToken(Arc<RwLock<Option<String>>>);

View File

@@ -546,7 +546,6 @@ impl PickerDelegate for LanguageModelPickerDelegate {
use feature_flags::FeatureFlagAppExt;
let plan = proto::Plan::ZedPro;
let is_trial = false;
Some(
h_flex()
@@ -558,7 +557,6 @@ impl PickerDelegate for LanguageModelPickerDelegate {
.justify_between()
.when(cx.has_flag::<ZedPro>(), |this| {
this.child(match plan {
// Already a Zed Pro subscriber
Plan::ZedPro => Button::new("zed-pro", "Zed Pro")
.icon(IconName::ZedAssistant)
.icon_size(IconSize::Small)
@@ -568,10 +566,9 @@ impl PickerDelegate for LanguageModelPickerDelegate {
window
.dispatch_action(Box::new(zed_actions::OpenAccountSettings), cx)
}),
// Free user
Plan::Free => Button::new(
Plan::Free | Plan::ZedProTrial => Button::new(
"try-pro",
if is_trial {
if plan == Plan::ZedProTrial {
"Upgrade to Pro"
} else {
"Try Pro"

View File

@@ -53,6 +53,7 @@ tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
ui.workspace = true
util.workspace = true
workspace-hack.workspace = true
zed_llm_client.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }

View File

@@ -16,18 +16,21 @@ use language_model::{
AuthenticateError, CloudModel, LanguageModel, LanguageModelCacheConfiguration, LanguageModelId,
LanguageModelKnownError, LanguageModelName, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelProviderTosView, LanguageModelRequest,
LanguageModelToolSchemaFormat, RateLimiter, ZED_CLOUD_PROVIDER_ID,
LanguageModelToolSchemaFormat, ModelRequestLimitReachedError, RateLimiter,
ZED_CLOUD_PROVIDER_ID,
};
use language_model::{
LanguageModelAvailability, LanguageModelCompletionEvent, LanguageModelProvider, LlmApiToken,
MaxMonthlySpendReachedError, PaymentRequiredError, RefreshLlmTokenListener,
};
use proto::Plan;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use serde_json::value::RawValue;
use settings::{Settings, SettingsStore};
use smol::Timer;
use smol::io::{AsyncReadExt, BufReader};
use std::str::FromStr as _;
use std::{
sync::{Arc, LazyLock},
time::Duration,
@@ -35,6 +38,7 @@ use std::{
use strum::IntoEnumIterator;
use thiserror::Error;
use ui::{TintColor, prelude::*};
use zed_llm_client::{CURRENT_PLAN_HEADER_NAME, SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME};
use crate::AllLanguageModelSettings;
use crate::provider::anthropic::{count_anthropic_tokens, into_anthropic};
@@ -551,6 +555,32 @@ impl CloudLanguageModel {
.is_some()
{
return Err(anyhow!(MaxMonthlySpendReachedError));
} else if status == StatusCode::FORBIDDEN
&& response
.headers()
.get(SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME)
.is_some()
{
if let Some("model_requests") = response
.headers()
.get(SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME)
.and_then(|resource| resource.to_str().ok())
{
if let Some(plan) = response
.headers()
.get(CURRENT_PLAN_HEADER_NAME)
.and_then(|plan| plan.to_str().ok())
.and_then(|plan| zed_llm_client::Plan::from_str(plan).ok())
{
let plan = match plan {
zed_llm_client::Plan::Free => Plan::Free,
zed_llm_client::Plan::ZedPro => Plan::ZedPro,
};
return Err(anyhow!(ModelRequestLimitReachedError { plan }));
}
}
return Err(anyhow!("Forbidden"));
} else if status.as_u16() >= 500 && status.as_u16() < 600 {
// If we encounter an error in the 500 range, retry after a delay.
// We've seen at least these in the wild from API providers:

View File

@@ -71,7 +71,7 @@ impl Anchor {
if self_excerpt_id == ExcerptId::min() || self_excerpt_id == ExcerptId::max() {
return Ordering::Equal;
}
if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
if let Some(excerpt) = snapshot.excerpt(self_excerpt_id) {
let text_cmp = self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer);
if text_cmp.is_ne() {
return text_cmp;

View File

@@ -5170,6 +5170,7 @@ impl MultiBufferSnapshot {
excerpt_id: ExcerptId,
text_anchor: text::Anchor,
) -> Option<Anchor> {
let excerpt_id = self.latest_excerpt_id(excerpt_id);
let locator = self.excerpt_locator_for_id(excerpt_id);
let mut cursor = self.excerpts.cursor::<Option<&Locator>>(&());
cursor.seek(locator, Bias::Left, &());
@@ -6041,7 +6042,7 @@ impl MultiBufferSnapshot {
return &entry.locator;
}
}
panic!("invalid excerpt id {:?}", id)
panic!("invalid excerpt id {id:?}")
}
}

View File

@@ -18,7 +18,7 @@ use text::{Point, PointUtf16};
use crate::{Project, ProjectPath, buffer_store::BufferStore, worktree_store::WorktreeStore};
mod breakpoints_in_file {
use language::BufferEvent;
use language::{BufferEvent, DiskState};
use super::*;
@@ -32,8 +32,9 @@ mod breakpoints_in_file {
impl BreakpointsInFile {
pub(super) fn new(buffer: Entity<Buffer>, cx: &mut Context<BreakpointStore>) -> Self {
let subscription =
Arc::from(cx.subscribe(&buffer, |_, buffer, event, cx| match event {
let subscription = Arc::from(cx.subscribe(
&buffer,
|breakpoint_store, buffer, event, cx| match event {
BufferEvent::Saved => {
if let Some(abs_path) = BreakpointStore::abs_path_from_buffer(&buffer, cx) {
cx.emit(BreakpointStoreEvent::BreakpointsUpdated(
@@ -42,8 +43,44 @@ mod breakpoints_in_file {
));
}
}
BufferEvent::FileHandleChanged => {
let entity_id = buffer.entity_id();
if buffer.read(cx).file().is_none_or(|f| f.disk_state() == DiskState::Deleted) {
breakpoint_store.breakpoints.retain(|_, breakpoints_in_file| {
breakpoints_in_file.buffer.entity_id() != entity_id
});
cx.notify();
return;
}
if let Some(abs_path) = BreakpointStore::abs_path_from_buffer(&buffer, cx) {
if breakpoint_store.breakpoints.contains_key(&abs_path) {
return;
}
if let Some(old_path) = breakpoint_store
.breakpoints
.iter()
.find(|(_, in_file)| in_file.buffer.entity_id() == entity_id)
.map(|values| values.0)
.cloned()
{
let Some(breakpoints_in_file) =
breakpoint_store.breakpoints.remove(&old_path) else {
log::error!("Couldn't get breakpoints in file from old path during buffer rename handling");
return;
};
breakpoint_store.breakpoints.insert(abs_path, breakpoints_in_file);
cx.notify();
}
}
}
_ => {}
}));
},
));
BreakpointsInFile {
buffer,

View File

@@ -3094,6 +3094,9 @@ impl Project {
.map(|lister| lister.term())
}
pub fn toolchain_store(&self) -> Option<Entity<ToolchainStore>> {
self.toolchain_store.clone()
}
pub fn activate_toolchain(
&self,
path: ProjectPath,

View File

@@ -55,6 +55,7 @@ impl ToolchainStore {
});
Self(ToolchainStoreInner::Local(entity, subscription))
}
pub(super) fn remote(project_id: u64, client: AnyProtoClient, cx: &mut App) -> Self {
Self(ToolchainStoreInner::Remote(
cx.new(|_| RemoteToolchainStore { client, project_id }),
@@ -285,7 +286,7 @@ struct LocalStore(WeakEntity<LocalToolchainStore>);
struct RemoteStore(WeakEntity<RemoteToolchainStore>);
#[derive(Clone)]
pub(crate) enum ToolchainStoreEvent {
pub enum ToolchainStoreEvent {
ToolchainActivated,
}

View File

@@ -18,6 +18,7 @@ message GetPrivateUserInfoResponse {
enum Plan {
Free = 0;
ZedPro = 1;
ZedProTrial = 2;
}
message UpdateUserPlan {

View File

@@ -36,7 +36,7 @@ use ui::{
IconWithIndicator, Indicator, PopoverMenu, Tooltip, h_flex, prelude::*,
};
use util::ResultExt;
use workspace::{BottomDockLayout, Workspace, notifications::NotifyResultExt};
use workspace::{Workspace, notifications::NotifyResultExt};
use zed_actions::{OpenBrowser, OpenRecent, OpenRemote};
pub use onboarding_banner::restore_banner;
@@ -210,7 +210,6 @@ impl Render for TitleBar {
.pr_1()
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.children(self.render_call_controls(window, cx))
.child(self.render_bottom_dock_layout_menu(cx))
.map(|el| {
let status = self.client.status();
let status = &*status.borrow();
@@ -302,7 +301,7 @@ impl TitleBar {
cx.notify()
}),
);
subscriptions.push(cx.subscribe(&project, |_, _, _, cx| cx.notify()));
subscriptions.push(cx.subscribe(&project, |_, _, _: &project::Event, cx| cx.notify()));
subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
subscriptions.push(cx.observe_window_activation(window, Self::window_activation_changed));
subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
@@ -623,101 +622,6 @@ impl TitleBar {
}
}
pub fn render_bottom_dock_layout_menu(&self, cx: &mut Context<Self>) -> impl IntoElement {
let workspace = self.workspace.upgrade().unwrap();
let current_layout = workspace.update(cx, |workspace, _cx| workspace.bottom_dock_layout());
PopoverMenu::new("layout-menu")
.trigger(
IconButton::new("toggle_layout", IconName::Layout)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Toggle Layout Menu")),
)
.anchor(gpui::Corner::TopRight)
.menu(move |window, cx| {
ContextMenu::build(window, cx, {
let workspace = workspace.clone();
move |menu, _, _| {
menu.label("Bottom Dock")
.separator()
.toggleable_entry(
"Contained",
current_layout == BottomDockLayout::Contained,
ui::IconPosition::End,
None,
{
let workspace = workspace.clone();
move |window, cx| {
workspace.update(cx, |workspace, cx| {
workspace.set_bottom_dock_layout(
BottomDockLayout::Contained,
window,
cx,
);
});
}
},
)
.toggleable_entry(
"Full",
current_layout == BottomDockLayout::Full,
ui::IconPosition::End,
None,
{
let workspace = workspace.clone();
move |window, cx| {
workspace.update(cx, |workspace, cx| {
workspace.set_bottom_dock_layout(
BottomDockLayout::Full,
window,
cx,
);
});
}
},
)
.toggleable_entry(
"Left Aligned",
current_layout == BottomDockLayout::LeftAligned,
ui::IconPosition::End,
None,
{
let workspace = workspace.clone();
move |window, cx| {
workspace.update(cx, |workspace, cx| {
workspace.set_bottom_dock_layout(
BottomDockLayout::LeftAligned,
window,
cx,
);
});
}
},
)
.toggleable_entry(
"Right Aligned",
current_layout == BottomDockLayout::RightAligned,
ui::IconPosition::End,
None,
{
let workspace = workspace.clone();
move |window, cx| {
workspace.update(cx, |workspace, cx| {
workspace.set_bottom_dock_layout(
BottomDockLayout::RightAligned,
window,
cx,
);
});
}
},
)
}
})
.into()
})
}
pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
let client = self.client.clone();
Button::new("sign_in", "Sign in")
@@ -751,6 +655,7 @@ impl TitleBar {
None => "",
Some(proto::Plan::Free) => "Free",
Some(proto::Plan::ZedPro) => "Pro",
Some(proto::Plan::ZedProTrial) => "Pro (Trial)",
}
),
zed_actions::OpenAccountSettings.boxed_clone(),

View File

@@ -1,4 +1,4 @@
use std::sync::Arc;
use std::{path::Path, sync::Arc};
use editor::Editor;
use gpui::{
@@ -6,7 +6,7 @@ use gpui::{
WeakEntity, Window, div,
};
use language::{Buffer, BufferEvent, LanguageName, Toolchain};
use project::{Project, ProjectPath, WorktreeId};
use project::{Project, ProjectPath, WorktreeId, toolchain_store::ToolchainStoreEvent};
use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, SharedString, Tooltip};
use workspace::{StatusItemView, Workspace, item::ItemHandle};
@@ -22,6 +22,28 @@ pub struct ActiveToolchain {
impl ActiveToolchain {
pub fn new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self {
if let Some(store) = workspace.project().read(cx).toolchain_store() {
cx.subscribe_in(
&store,
window,
|this, _, _: &ToolchainStoreEvent, window, cx| {
let editor = this
.workspace
.update(cx, |workspace, cx| {
workspace
.active_item(cx)
.and_then(|item| item.downcast::<Editor>())
})
.ok()
.flatten();
if let Some(editor) = editor {
this.active_toolchain.take();
this.update_lister(editor, window, cx);
}
},
)
.detach();
}
Self {
active_toolchain: None,
active_buffer: None,
@@ -57,12 +79,19 @@ impl ActiveToolchain {
this.term = term;
cx.notify();
});
let worktree_id = active_file
.update(cx, |this, cx| Some(this.file()?.worktree_id(cx)))
let (worktree_id, path) = active_file
.update(cx, |this, cx| {
this.file().and_then(|file| {
Some((
file.worktree_id(cx),
Arc::<Path>::from(file.path().parent()?),
))
})
})
.ok()
.flatten()?;
let toolchain =
Self::active_toolchain(workspace, worktree_id, language_name, cx).await?;
Self::active_toolchain(workspace, worktree_id, path, language_name, cx).await?;
let _ = this.update(cx, |this, cx| {
this.active_toolchain = Some(toolchain);
@@ -101,6 +130,7 @@ impl ActiveToolchain {
fn active_toolchain(
workspace: WeakEntity<Workspace>,
worktree_id: WorktreeId,
relative_path: Arc<Path>,
language_name: LanguageName,
cx: &mut AsyncWindowContext,
) -> Task<Option<Toolchain>> {
@@ -114,7 +144,7 @@ impl ActiveToolchain {
this.project().read(cx).active_toolchain(
ProjectPath {
worktree_id,
path: Arc::from("".as_ref()),
path: relative_path.clone(),
},
language_name.clone(),
cx,
@@ -133,7 +163,7 @@ impl ActiveToolchain {
project.read(cx).available_toolchains(
ProjectPath {
worktree_id,
path: Arc::from("".as_ref()),
path: relative_path.clone(),
},
language_name,
cx,
@@ -144,7 +174,12 @@ impl ActiveToolchain {
if let Some(toolchain) = toolchains.toolchains.first() {
// Since we don't have a selected toolchain, pick one for user here.
workspace::WORKSPACE_DB
.set_toolchain(workspace_id, worktree_id, "".to_owned(), toolchain.clone())
.set_toolchain(
workspace_id,
worktree_id,
relative_path.to_string_lossy().into_owned(),
toolchain.clone(),
)
.await
.ok()?;
project
@@ -152,7 +187,7 @@ impl ActiveToolchain {
this.activate_toolchain(
ProjectPath {
worktree_id,
path: Arc::from("".as_ref()),
path: relative_path,
},
toolchain.clone(),
cx,

View File

@@ -50,6 +50,7 @@ impl ToolchainSelector {
let language_name = buffer.read(cx).language()?.name();
let worktree_id = buffer.read(cx).file()?.worktree_id(cx);
let relative_path: Arc<Path> = Arc::from(buffer.read(cx).file()?.path().parent()?);
let worktree_root_path = project
.read(cx)
.worktree_for_id(worktree_id, cx)?
@@ -58,8 +59,9 @@ impl ToolchainSelector {
let workspace_id = workspace.database_id()?;
let weak = workspace.weak_handle();
cx.spawn_in(window, async move |workspace, cx| {
let as_str = relative_path.to_string_lossy().into_owned();
let active_toolchain = workspace::WORKSPACE_DB
.toolchain(workspace_id, worktree_id, language_name.clone())
.toolchain(workspace_id, worktree_id, as_str, language_name.clone())
.await
.ok()
.flatten();
@@ -72,6 +74,7 @@ impl ToolchainSelector {
active_toolchain,
worktree_id,
worktree_root_path,
relative_path,
language_name,
window,
cx,
@@ -91,6 +94,7 @@ impl ToolchainSelector {
active_toolchain: Option<Toolchain>,
worktree_id: WorktreeId,
worktree_root: Arc<Path>,
relative_path: Arc<Path>,
language_name: LanguageName,
window: &mut Window,
cx: &mut Context<Self>,
@@ -104,6 +108,7 @@ impl ToolchainSelector {
worktree_id,
worktree_root,
project,
relative_path,
language_name,
window,
cx,
@@ -137,6 +142,7 @@ pub struct ToolchainSelectorDelegate {
workspace: WeakEntity<Workspace>,
worktree_id: WorktreeId,
worktree_abs_path_root: Arc<Path>,
relative_path: Arc<Path>,
placeholder_text: Arc<str>,
_fetch_candidates_task: Task<Option<()>>,
}
@@ -149,6 +155,7 @@ impl ToolchainSelectorDelegate {
worktree_id: WorktreeId,
worktree_abs_path_root: Arc<Path>,
project: Entity<Project>,
relative_path: Arc<Path>,
language_name: LanguageName,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
@@ -162,17 +169,26 @@ impl ToolchainSelectorDelegate {
})
.ok()?
.await?;
let placeholder_text = format!("Select a {}", term.to_lowercase()).into();
let relative_path = this
.update(cx, |this, _| this.delegate.relative_path.clone())
.ok()?;
let placeholder_text = format!(
"Select a {} for `{}`…",
term.to_lowercase(),
relative_path.to_string_lossy()
)
.into();
let _ = this.update_in(cx, move |this, window, cx| {
this.delegate.placeholder_text = placeholder_text;
this.refresh_placeholder(window, cx);
});
let available_toolchains = project
.update(cx, |this, cx| {
this.available_toolchains(
ProjectPath {
worktree_id,
path: Arc::from("".as_ref()),
path: relative_path.clone(),
},
language_name,
cx,
@@ -211,6 +227,7 @@ impl ToolchainSelectorDelegate {
worktree_id,
worktree_abs_path_root,
placeholder_text,
relative_path,
_fetch_candidates_task,
}
}
@@ -246,19 +263,18 @@ impl PickerDelegate for ToolchainSelectorDelegate {
{
let workspace = self.workspace.clone();
let worktree_id = self.worktree_id;
let path = self.relative_path.clone();
let relative_path = self.relative_path.to_string_lossy().into_owned();
cx.spawn_in(window, async move |_, cx| {
workspace::WORKSPACE_DB
.set_toolchain(workspace_id, worktree_id, "".to_owned(), toolchain.clone())
.set_toolchain(workspace_id, worktree_id, relative_path, toolchain.clone())
.await
.log_err();
workspace
.update(cx, |this, cx| {
this.project().update(cx, |this, cx| {
this.activate_toolchain(
ProjectPath {
worktree_id,
path: Arc::from("".as_ref()),
},
ProjectPath { worktree_id, path },
toolchain,
cx,
)

View File

@@ -160,7 +160,11 @@ impl Render for Tooltip {
}),
)
.when_some(self.meta.clone(), |this, meta| {
this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted))
this.child(
div()
.max_w_72()
.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted)),
)
})
})
}

View File

@@ -132,7 +132,15 @@ fn scroll_editor(
let max_visible_row = top.row().0.saturating_add(
(visible_line_count as u32).saturating_sub(1 + vertical_scroll_margin),
);
let max_row = DisplayRow(map.max_point().row().0.max(max_visible_row));
// scroll off the end.
let max_row = if top.row().0 + visible_line_count as u32 >= map.max_point().row().0 {
map.max_point().row()
} else {
DisplayRow(
(top.row().0 + visible_line_count as u32)
.saturating_sub(1 + vertical_scroll_margin),
)
};
let new_row = if full_page_up {
// Special-casing ctrl-b/page-up, which is special-cased by Vim, it seems
@@ -372,14 +380,14 @@ mod test {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_scroll_height(10).await;
cx.neovim.set_option(&format!("scrolloff={}", 0)).await;
let content = "ˇ".to_owned() + &sample_text(26, 2, 'a');
cx.set_shared_state(&content).await;
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<EditorSettings>(cx, |s| {
s.scroll_beyond_last_line = Some(ScrollBeyondLastLine::Off)
s.scroll_beyond_last_line = Some(ScrollBeyondLastLine::Off);
// s.vertical_scroll_margin = Some(0.);
});
});
@@ -395,4 +403,24 @@ mod test {
cx.simulate_shared_keystrokes("ctrl-u").await;
cx.shared_state().await.assert_matches();
}
#[gpui::test]
async fn test_ctrl_y_e(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_scroll_height(10).await;
let content = "ˇ".to_owned() + &sample_text(26, 2, 'a');
cx.set_shared_state(&content).await;
for _ in 0..8 {
cx.simulate_shared_keystrokes("ctrl-e").await;
cx.shared_state().await.assert_matches();
}
for _ in 0..8 {
cx.simulate_shared_keystrokes("ctrl-y").await;
cx.shared_state().await.assert_matches();
}
}
}

View File

@@ -0,0 +1,35 @@
{"SetOption":{"value":"scrolloff=3"}}
{"SetOption":{"value":"lines=12"}}
{"Put":{"state":"ˇaa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz"}}
{"Key":"ctrl-e"}
{"Get":{"state":"aa\nbb\ncc\ndd\nˇee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
{"Key":"ctrl-e"}
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nˇff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
{"Key":"ctrl-e"}
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\nˇgg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
{"Key":"ctrl-e"}
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nˇhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
{"Key":"ctrl-e"}
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nˇii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
{"Key":"ctrl-e"}
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\nˇjj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
{"Key":"ctrl-e"}
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nˇkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
{"Key":"ctrl-e"}
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nˇll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
{"Key":"ctrl-y"}
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nˇll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
{"Key":"ctrl-y"}
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nˇll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
{"Key":"ctrl-y"}
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nˇll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
{"Key":"ctrl-y"}
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nˇkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
{"Key":"ctrl-y"}
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\nˇjj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
{"Key":"ctrl-y"}
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nˇii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
{"Key":"ctrl-y"}
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nˇhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
{"Key":"ctrl-y"}
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\nˇgg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}

View File

@@ -1,6 +1,5 @@
{"SetOption":{"value":"scrolloff=3"}}
{"SetOption":{"value":"lines=12"}}
{"SetOption":{"value":"scrolloff=0"}}
{"Put":{"state":"ˇaa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz"}}
{"Key":"shift-g"}
{"Key":"k"}
@@ -10,4 +9,4 @@
{"Key":"shift-g"}
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nˇzz","mode":"Normal"}}
{"Key":"ctrl-u"}
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nˇuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nˇrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}

View File

@@ -0,0 +1,20 @@
[package]
name = "web_search"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/web_search.rs"
[dependencies]
anyhow.workspace = true
collections.workspace = true
gpui.workspace = true
serde.workspace = true
workspace-hack.workspace = true
zed_llm_client.workspace = true

View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -0,0 +1,64 @@
use anyhow::Result;
use collections::HashMap;
use gpui::{App, AppContext as _, Context, Entity, Global, SharedString, Task};
use std::sync::Arc;
use zed_llm_client::WebSearchResponse;
pub fn init(cx: &mut App) {
let registry = cx.new(|_cx| WebSearchRegistry::default());
cx.set_global(GlobalWebSearchRegistry(registry));
}
#[derive(Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)]
pub struct WebSearchProviderId(pub SharedString);
pub trait WebSearchProvider {
fn id(&self) -> WebSearchProviderId;
fn search(&self, query: String, cx: &mut App) -> Task<Result<WebSearchResponse>>;
}
struct GlobalWebSearchRegistry(Entity<WebSearchRegistry>);
impl Global for GlobalWebSearchRegistry {}
#[derive(Default)]
pub struct WebSearchRegistry {
providers: HashMap<WebSearchProviderId, Arc<dyn WebSearchProvider>>,
active_provider: Option<Arc<dyn WebSearchProvider>>,
}
impl WebSearchRegistry {
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalWebSearchRegistry>().0.clone()
}
pub fn read_global(cx: &App) -> &Self {
cx.global::<GlobalWebSearchRegistry>().0.read(cx)
}
pub fn providers(&self) -> impl Iterator<Item = &Arc<dyn WebSearchProvider>> {
self.providers.values()
}
pub fn active_provider(&self) -> Option<Arc<dyn WebSearchProvider>> {
self.active_provider.clone()
}
pub fn set_active_provider(&mut self, provider: Arc<dyn WebSearchProvider>) {
self.active_provider = Some(provider.clone());
self.providers.insert(provider.id(), provider);
}
pub fn register_provider<T: WebSearchProvider + 'static>(
&mut self,
provider: T,
_cx: &mut Context<Self>,
) {
let id = provider.id();
let provider = Arc::new(provider);
self.providers.insert(id.clone(), provider.clone());
if self.active_provider.is_none() {
self.active_provider = Some(provider);
}
}
}

View File

@@ -0,0 +1,26 @@
[package]
name = "web_search_providers"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/web_search_providers.rs"
[dependencies]
anyhow.workspace = true
client.workspace = true
feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
http_client.workspace = true
language_model.workspace = true
serde.workspace = true
serde_json.workspace = true
web_search.workspace = true
workspace-hack.workspace = true
zed_llm_client.workspace = true

View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -0,0 +1,103 @@
use std::sync::Arc;
use anyhow::{Context as _, Result, anyhow};
use client::Client;
use futures::AsyncReadExt as _;
use gpui::{App, AppContext, Context, Entity, Subscription, Task};
use http_client::{HttpClient, Method};
use language_model::{LlmApiToken, RefreshLlmTokenListener};
use web_search::{WebSearchProvider, WebSearchProviderId};
use zed_llm_client::{WebSearchBody, WebSearchResponse};
pub struct CloudWebSearchProvider {
state: Entity<State>,
}
impl CloudWebSearchProvider {
pub fn new(client: Arc<Client>, cx: &mut App) -> Self {
let state = cx.new(|cx| State::new(client, cx));
Self { state }
}
}
pub struct State {
client: Arc<Client>,
llm_api_token: LlmApiToken,
_llm_token_subscription: Subscription,
}
impl State {
pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx);
Self {
client,
llm_api_token: LlmApiToken::default(),
_llm_token_subscription: cx.subscribe(
&refresh_llm_token_listener,
|this, _, _event, cx| {
let client = this.client.clone();
let llm_api_token = this.llm_api_token.clone();
cx.spawn(async move |_this, _cx| {
llm_api_token.refresh(&client).await?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
},
),
}
}
}
impl WebSearchProvider for CloudWebSearchProvider {
fn id(&self) -> WebSearchProviderId {
WebSearchProviderId("zed.dev".into())
}
fn search(&self, query: String, cx: &mut App) -> Task<Result<WebSearchResponse>> {
let state = self.state.read(cx);
let client = state.client.clone();
let llm_api_token = state.llm_api_token.clone();
let body = WebSearchBody { query };
cx.background_spawn(async move { perform_web_search(client, llm_api_token, body).await })
}
}
async fn perform_web_search(
client: Arc<Client>,
llm_api_token: LlmApiToken,
body: WebSearchBody,
) -> Result<WebSearchResponse> {
let http_client = &client.http_client();
let token = llm_api_token.acquire(&client).await?;
let request_builder = http_client::Request::builder().method(Method::POST);
let request_builder = if let Ok(web_search_url) = std::env::var("ZED_WEB_SEARCH_URL") {
request_builder.uri(web_search_url)
} else {
request_builder.uri(http_client.build_zed_llm_url("/web_search", &[])?.as_ref())
};
let request = request_builder
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {token}"))
.body(serde_json::to_string(&body)?.into())?;
let mut response = http_client
.send(request)
.await
.context("failed to send web search request")?;
if response.status().is_success() {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
return Ok(serde_json::from_str(&body)?);
} else {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
return Err(anyhow!(
"error performing web search.\nStatus: {:?}\nBody: {body}",
response.status(),
));
}
}

View File

@@ -0,0 +1,35 @@
mod cloud;
use client::Client;
use feature_flags::{FeatureFlagAppExt, ZedProWebSearchTool};
use gpui::{App, Context};
use std::sync::Arc;
use web_search::WebSearchRegistry;
pub fn init(client: Arc<Client>, cx: &mut App) {
let registry = WebSearchRegistry::global(cx);
registry.update(cx, |registry, cx| {
register_web_search_providers(registry, client, cx);
});
}
fn register_web_search_providers(
_registry: &mut WebSearchRegistry,
client: Arc<Client>,
cx: &mut Context<WebSearchRegistry>,
) {
cx.observe_flag::<ZedProWebSearchTool, _>({
let client = client.clone();
move |is_enabled, cx| {
if is_enabled {
WebSearchRegistry::global(cx).update(cx, |registry, cx| {
registry.register_provider(
cloud::CloudWebSearchProvider::new(client.clone(), cx),
cx,
);
});
}
}
})
.detach();
}

View File

@@ -1334,17 +1334,18 @@ impl WorkspaceDb {
&self,
workspace_id: WorkspaceId,
worktree_id: WorktreeId,
relative_path: String,
language_name: LanguageName,
) -> Result<Option<Toolchain>> {
self.write(move |this| {
let mut select = this
.select_bound(sql!(
SELECT name, path, raw_json FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ?
SELECT name, path, raw_json FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ? AND relative_path = ?
))
.context("Preparing insertion")?;
let toolchain: Vec<(String, String, String)> =
select((workspace_id, language_name.as_ref().to_string(), worktree_id.to_usize()))?;
select((workspace_id, language_name.as_ref().to_string(), worktree_id.to_usize(), relative_path))?;
Ok(toolchain.into_iter().next().and_then(|(name, path, raw_json)| Some(Toolchain {
name: name.into(),

View File

@@ -133,6 +133,8 @@ util.workspace = true
uuid.workspace = true
vim.workspace = true
vim_mode_setting.workspace = true
web_search.workspace = true
web_search_providers.workspace = true
welcome.workspace = true
workspace.workspace = true
zed_actions.workspace = true

View File

@@ -490,6 +490,8 @@ fn main() {
app_state.fs.clone(),
cx,
);
web_search::init(cx);
web_search_providers::init(app_state.client.clone(), cx);
snippet_provider::init(cx);
inline_completion_registry::init(
app_state.client.clone(),

View File

@@ -4258,6 +4258,8 @@ mod tests {
app_state.fs.clone(),
cx,
);
web_search::init(cx);
web_search_providers::init(app_state.client.clone(), cx);
let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx);
assistant::init(
app_state.fs.clone(),

View File

@@ -75,6 +75,46 @@ Non-negative `float` values
`float` values
## Bottom Dock Layout
- Description: Control the layout of the bottom dock, relative to the left and right docks
- Setting: `bottom_dock_layout`
- Default: `"contained"`
**Options**
1. Contain the bottom dock, giving the full height of the window to the left and right docks
```json
{
"bottom_dock_layout": "contained"
}
```
2. Give the bottom dock the full width of the window, truncating the left and right docks
```json
{
"bottom_dock_layout": "full"
}
```
3. Left align the bottom dock, truncating the left dock and giving the right dock the full height of the window
```json
{
"bottom_dock_layout": "left_aligned"
}
```
3. Right align the bottom dock, giving the left dock the full height of the window and truncating the right dock.
```json
{
"bottom_dock_layout": "right_aligned"
}
```
## Auto Install extensions
- Description: Define extensions to be autoinstalled or never be installed.