Merge branch 'main' of https://github.com/zed-industries/zed into martin/ai-233-fix-issue-with-rendering-file-paths

This commit is contained in:
Martin Ye 2026-05-15 11:30:54 -07:00
commit 534506f94c
33 changed files with 1453 additions and 830 deletions

View file

@ -44,6 +44,7 @@ jobs:
uses: zed-industries/zed/.github/workflows/deploy_docs.yml@main
secrets:
DOCS_AMPLITUDE_API_KEY: ${{ secrets.DOCS_AMPLITUDE_API_KEY }}
DOCS_CONSENT_IO_INSTANCE: ${{ secrets.DOCS_CONSENT_IO_INSTANCE }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
with:

View file

@ -16,6 +16,9 @@ on:
DOCS_AMPLITUDE_API_KEY:
description: DOCS_AMPLITUDE_API_KEY
required: true
DOCS_CONSENT_IO_INSTANCE:
description: DOCS_CONSENT_IO_INSTANCE
required: true
CLOUDFLARE_API_TOKEN:
description: CLOUDFLARE_API_TOKEN
required: true
@ -39,6 +42,7 @@ jobs:
runs-on: namespace-profile-16x32-ubuntu-2204
env:
DOCS_AMPLITUDE_API_KEY: ${{ secrets.DOCS_AMPLITUDE_API_KEY }}
DOCS_CONSENT_IO_INSTANCE: ${{ secrets.DOCS_CONSENT_IO_INSTANCE }}
CC: clang
CXX: clang++
steps:

View file

@ -13,6 +13,7 @@ jobs:
uses: zed-industries/zed/.github/workflows/deploy_docs.yml@main
secrets:
DOCS_AMPLITUDE_API_KEY: ${{ secrets.DOCS_AMPLITUDE_API_KEY }}
DOCS_CONSENT_IO_INSTANCE: ${{ secrets.DOCS_CONSENT_IO_INSTANCE }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
with:

View file

@ -668,6 +668,7 @@ jobs:
runs-on: namespace-profile-16x32-ubuntu-2204
env:
DOCS_AMPLITUDE_API_KEY: ${{ secrets.DOCS_AMPLITUDE_API_KEY }}
DOCS_CONSENT_IO_INSTANCE: ${{ secrets.DOCS_CONSENT_IO_INSTANCE }}
CC: clang
CXX: clang++
steps:

3
Cargo.lock generated
View file

@ -81,6 +81,7 @@ dependencies = [
"futures 0.3.32",
"git",
"gpui",
"indoc",
"language",
"log",
"pretty_assertions",
@ -7930,6 +7931,7 @@ dependencies = [
"bytemuck",
"collections",
"cosmic-text",
"criterion",
"etagere",
"gpui",
"gpui_util",
@ -7942,6 +7944,7 @@ dependencies = [
"raw-window-handle",
"smallvec",
"swash",
"unicode-segmentation",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",

View file

@ -33,12 +33,12 @@ watch.workspace = true
[dev-dependencies]
buffer_diff = { workspace = true, features = ["test-support"] }
git.workspace = true
collections = { workspace = true, features = ["test-support"] }
clock = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] }
ctor.workspace = true
git.workspace = true
gpui = { workspace = true, features = ["test-support"] }
indoc.workspace = true
language = { workspace = true, features = ["test-support"] }
log.workspace = true
pretty_assertions.workspace = true

View file

@ -387,6 +387,11 @@ impl ActionLog {
let git_diff_base = git_diff.read(cx).base_text(cx).as_rope().clone();
let buffer_text = tracked_buffer.snapshot.as_rope().clone();
anyhow::Ok(cx.background_spawn(async move {
if buffer_text.len() == git_diff_base.len()
&& buffer_text.chars_at(0).eq(git_diff_base.chars_at(0))
{
return (Arc::<str>::from(git_diff_base.to_string()), git_diff_base);
}
let mut old_unreviewed_edits = old_unreviewed_edits.into_iter().peekable();
let committed_edits = language::line_diff(
&agent_diff_base.to_string(),
@ -1320,6 +1325,7 @@ mod tests {
use super::*;
use buffer_diff::DiffHunkStatusKind;
use gpui::TestAppContext;
use indoc::indoc;
use language::Point;
use project::{FakeFs, Fs, Project, RemoveOptions};
use rand::prelude::*;
@ -2703,6 +2709,86 @@ mod tests {
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
}
#[gpui::test]
async fn test_keep_edits_on_commit_with_shifted_diff_boundaries(cx: &mut TestAppContext) {
init_test(cx);
let initial_text = indoc! {"
use crate::{Alpha, Beta};
fn keep() {
work();
}
fn remove() {
work();
}
fn after() {
work();
}
"};
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/project"),
json!({
".git": {},
"file.rs": initial_text,
}),
)
.await;
fs.set_head_for_repo(
path!("/project/.git").as_ref(),
&[("file.rs", initial_text.into())],
"0000000",
);
cx.run_until_parked();
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let file_path = project
.read_with(cx, |project, cx| {
project.find_project_path(path!("/project/file.rs"), cx)
})
.unwrap();
let buffer = project
.update(cx, |project, cx| project.open_buffer(file_path, cx))
.await
.unwrap();
let final_text = indoc! {"
use crate::{Alpha};
fn keep() {
work();
}
fn after() {
work();
}
"};
cx.update(|cx| {
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| {
buffer.set_text(final_text, cx);
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
cx.run_until_parked();
assert!(!unreviewed_hunks(&action_log, cx).is_empty());
fs.set_head_for_repo(
path!("/project/.git").as_ref(),
&[("file.rs", final_text.into())],
"0000001",
);
cx.run_until_parked();
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
}
/// Regression test: when head_commit updates before the BufferDiff's base
/// text does, an intermediate DiffChanged (e.g. from a buffer-edit diff
/// recalculation) must NOT consume the commit signal. The subscription

View file

@ -60,6 +60,7 @@ use collections::HashMap;
use editor::{Editor, MultiBuffer};
use extension::ExtensionEvents;
use extension_host::ExtensionStore;
use feature_flags::{FeatureFlagAppExt as _, SkillsFeatureFlag};
use fs::Fs;
use gpui::{
Action, Anchor, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, ClipboardItem,
@ -2094,7 +2095,15 @@ impl AgentPanel {
let draft = self.ensure_draft(source, window, cx);
if let BaseView::AgentThread { conversation_view } = &self.base_view {
if conversation_view.entity_id() == draft.entity_id() {
if focus {
// If we're already viewing the draft as the base view but an
// overlay (e.g. Settings) is covering it, clear the overlay
// so the user actually sees the draft they asked for.
// Otherwise pressing "New Thread" from the Settings panel is
// a silent no-op because the early return below would leave
// the overlay on top of the draft.
if self.overlay_view.is_some() {
self.clear_overlay(focus, window, cx);
} else if focus {
self.focus_handle(cx).focus(window, cx);
}
return;
@ -4170,6 +4179,12 @@ impl AgentPanel {
.with_handle(self.agent_panel_menu_handle.clone())
.menu({
move |window, cx| {
// When the Skills feature flag is on, hide the legacy Rules menu entry.
// The flag is read from a global store populated asynchronously, and
// this menu builder runs on every open, so the latest resolved value is
// reflected when the user clicks the ellipsis.
let skills_enabled = cx.has_flag::<SkillsFeatureFlag>();
Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
menu = menu.context(focus_handle.clone());
@ -4204,9 +4219,13 @@ impl AgentPanel {
}),
)
.action("Add Custom Server…", Box::new(AddContextServer))
.separator()
.action("Rules", Box::new(OpenRulesLibrary::default()))
.action("Profiles", Box::new(ManageProfiles::default()));
.separator();
if !skills_enabled {
menu = menu.action("Rules", Box::new(OpenRulesLibrary::default()));
}
menu = menu.action("Profiles", Box::new(ManageProfiles::default()));
}
menu = menu
@ -6683,6 +6702,63 @@ mod tests {
});
}
#[gpui::test]
async fn test_new_thread_dismisses_settings_overlay(cx: &mut TestAppContext) {
let (panel, mut cx) = setup_panel(cx).await;
// Put the panel on its ephemeral new-draft view so the base view
// already contains the draft that `NewThread` would activate.
panel.update_in(&mut cx, |panel, window, cx| {
panel.activate_new_thread(true, AgentThreadSource::AgentPanel, window, cx);
});
cx.run_until_parked();
panel.read_with(&cx, |panel, cx| {
assert!(
panel.active_view_is_new_draft(cx),
"precondition: base view should be the ephemeral draft"
);
assert!(!panel.is_overlay_open());
});
// Simulate the Settings overlay being open on top of the draft.
// We don't go through `open_configuration` here because it would
// build provider configuration views, which call into
// `LanguageModelProvider::configuration_view` — unimplemented for
// the fake provider used in tests. The bug being exercised lives
// entirely in the overlay/base-view bookkeeping, so toggling the
// overlay flag directly is sufficient.
panel.update_in(&mut cx, |panel, window, cx| {
panel.set_overlay(OverlayView::Configuration, true, window, cx);
});
cx.run_until_parked();
panel.read_with(&cx, |panel, _cx| {
assert!(
panel.is_overlay_open(),
"precondition: Settings overlay should be open"
);
});
// Dispatching `NewThread` while Settings is open must dismiss the
// overlay so the user actually sees the new thread. Previously
// this was a silent no-op: `activate_draft` early-returned without
// clearing the overlay because the base view already held the
// draft.
panel.update_in(&mut cx, |panel, window, cx| {
panel.new_thread(&NewThread, window, cx);
});
cx.run_until_parked();
panel.read_with(&cx, |panel, cx| {
assert!(
!panel.is_overlay_open(),
"Settings overlay should be dismissed when invoking NewThread"
);
assert!(panel.active_view_is_new_draft(cx));
});
}
#[gpui::test]
async fn test_terminal_title_omits_placeholder_title(cx: &mut TestAppContext) {
let (panel, mut cx) = setup_panel(cx).await;

View file

@ -651,6 +651,12 @@ fn update_command_palette_filter(cx: &mut App) {
.edit_predictions
.provider;
// The Skills feature flag is loaded asynchronously, so this value may
// be `false` before flags resolve. `update_command_palette_filter`
// gets re-run from `cx.on_flags_ready` (see `init`), which means the
// filter is reapplied with the correct value once flags arrive.
let skills_enabled = cx.has_flag::<SkillsFeatureFlag>();
CommandPaletteFilter::update_global(cx, |filter, _| {
use editor::actions::{
AcceptEditPrediction, AcceptNextLineEditPrediction, AcceptNextWordEditPrediction,
@ -667,6 +673,8 @@ fn update_command_palette_filter(cx: &mut App) {
TypeId::of::<ToggleEditPrediction>(),
];
let open_rules_library_action = [TypeId::of::<zed_actions::assistant::OpenRulesLibrary>()];
if disable_ai {
filter.hide_namespace("agent");
filter.hide_namespace("agents");
@ -715,6 +723,17 @@ fn update_command_palette_filter(cx: &mut App) {
filter.show_namespace("multi_workspace");
}
// Hide `assistant: open rules library` when Skills are enabled —
// Rules are surfaced through the Skills UI in that case. Applied
// after the disable-ai / agent-enabled branches so it overrides
// the `show_namespace("assistant")` call above without affecting
// the rest of that namespace's actions.
if !disable_ai && skills_enabled {
filter.hide_action_types(&open_rules_library_action);
} else {
filter.show_action_types(open_rules_library_action.iter());
}
});
}

View file

@ -540,30 +540,6 @@ impl Model {
}
}
pub fn cache_configuration(&self) -> Option<BedrockModelCacheConfiguration> {
match self {
Self::ClaudeSonnet4
| Self::ClaudeSonnet4_5
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_6
| Self::ClaudeOpus4_7
| Self::ClaudeSonnet4_6 => Some(BedrockModelCacheConfiguration {
max_cache_anchors: 4,
min_total_token: 1024,
}),
Self::ClaudeHaiku4_5 => Some(BedrockModelCacheConfiguration {
max_cache_anchors: 4,
min_total_token: 2048,
}),
Self::Custom {
cache_configuration,
..
} => cache_configuration.clone(),
_ => None,
}
}
pub fn supports_thinking(&self) -> bool {
matches!(
self,

View file

@ -3,18 +3,10 @@ use super::*;
impl Database {
/// Creates a new user.
#[cfg(feature = "test-support")]
pub async fn create_user(
&self,
email_address: &str,
name: Option<&str>,
admin: bool,
params: NewUserParams,
) -> Result<NewUserResult> {
pub async fn create_user(&self, admin: bool, params: NewUserParams) -> Result<NewUserResult> {
self.transaction(|tx| async {
let tx = tx;
let user = user::Entity::insert(user::ActiveModel {
email_address: ActiveValue::set(Some(email_address.into())),
name: ActiveValue::set(name.map(|s| s.into())),
github_login: ActiveValue::set(params.github_login.clone()),
github_user_id: ActiveValue::set(params.github_user_id),
admin: ActiveValue::set(admin),
@ -22,11 +14,7 @@ impl Database {
})
.on_conflict(
OnConflict::column(user::Column::GithubUserId)
.update_columns([
user::Column::Admin,
user::Column::EmailAddress,
user::Column::GithubLogin,
])
.update_columns([user::Column::Admin, user::Column::GithubLogin])
.to_owned(),
)
.exec_with_returning(&*tx)

View file

@ -1,5 +1,4 @@
use crate::db::UserId;
use chrono::NaiveDateTime;
use sea_orm::entity::prelude::*;
use serde::Serialize;
@ -11,12 +10,8 @@ pub struct Model {
pub id: UserId,
pub github_login: String,
pub github_user_id: i32,
pub github_user_created_at: Option<NaiveDateTime>,
pub email_address: Option<String>,
pub name: Option<String>,
pub admin: bool,
pub connected_once: bool,
pub created_at: NaiveDateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

@ -209,8 +209,6 @@ static GITHUB_USER_ID: AtomicI32 = AtomicI32::new(5);
async fn new_test_user(db: &Arc<Database>, email: &str) -> UserId {
db.create_user(
email,
None,
false,
NewUserParams {
github_login: email[0..email.find('@').unwrap()].to_string(),

View file

@ -13,8 +13,6 @@ test_both_dbs!(
async fn test_channel_buffers(db: &Arc<Database>) {
let a_id = db
.create_user(
"user_a@example.com",
None,
false,
NewUserParams {
github_login: "user_a".into(),
@ -26,8 +24,6 @@ async fn test_channel_buffers(db: &Arc<Database>) {
.user_id;
let b_id = db
.create_user(
"user_b@example.com",
None,
false,
NewUserParams {
github_login: "user_b".into(),
@ -41,8 +37,6 @@ async fn test_channel_buffers(db: &Arc<Database>) {
// This user will not be a part of the channel
let c_id = db
.create_user(
"user_c@example.com",
None,
false,
NewUserParams {
github_login: "user_c".into(),
@ -188,8 +182,6 @@ test_both_dbs!(
async fn test_channel_buffers_last_operations(db: &Database) {
let user_id = db
.create_user(
"user_a@example.com",
None,
false,
NewUserParams {
github_login: "user_a".into(),
@ -201,8 +193,6 @@ async fn test_channel_buffers_last_operations(db: &Database) {
.user_id;
let observer_id = db
.create_user(
"user_b@example.com",
None,
false,
NewUserParams {
github_login: "user_b".into(),

View file

@ -263,8 +263,6 @@ async fn test_channel_renames(db: &Arc<Database>) {
let user_1 = db
.create_user(
"user1@example.com",
None,
false,
NewUserParams {
github_login: "user1".into(),
@ -277,8 +275,6 @@ async fn test_channel_renames(db: &Arc<Database>) {
let user_2 = db
.create_user(
"user2@example.com",
None,
false,
NewUserParams {
github_login: "user2".into(),
@ -314,8 +310,6 @@ test_both_dbs!(
async fn test_db_channel_moving(db: &Arc<Database>) {
let a_id = db
.create_user(
"user1@example.com",
None,
false,
NewUserParams {
github_login: "user1".into(),
@ -404,8 +398,6 @@ test_both_dbs!(
async fn test_channel_reordering(db: &Arc<Database>) {
let admin_id = db
.create_user(
"admin@example.com",
None,
false,
NewUserParams {
github_login: "admin".into(),
@ -418,8 +410,6 @@ async fn test_channel_reordering(db: &Arc<Database>) {
let user_id = db
.create_user(
"user@example.com",
None,
false,
NewUserParams {
github_login: "user".into(),
@ -599,8 +589,6 @@ test_both_dbs!(
async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
let user_id = db
.create_user(
"user1@example.com",
None,
false,
NewUserParams {
github_login: "user1".into(),

View file

@ -18,8 +18,6 @@ async fn test_add_contacts(db: &Arc<Database>) {
for i in 0..3 {
user_ids.push(
db.create_user(
&format!("user{i}@example.com"),
None,
false,
NewUserParams {
github_login: format!("user{i}"),
@ -178,8 +176,6 @@ async fn test_project_count(db: &Arc<Database>) {
let user1 = db
.create_user(
"admin@example.com",
None,
true,
NewUserParams {
github_login: "admin".into(),
@ -190,8 +186,6 @@ async fn test_project_count(db: &Arc<Database>) {
.unwrap();
let user2 = db
.create_user(
"user@example.com",
None,
false,
NewUserParams {
github_login: "user".into(),

View file

@ -225,8 +225,6 @@ impl<T: RandomizedTest> TestPlan<T> {
.app_state
.db
.create_user(
&format!("{username}@example.com"),
None,
false,
NewUserParams {
github_login: username.clone(),

View file

@ -535,23 +535,12 @@ impl ConfigurationView {
label: impl Into<SharedString>,
edit_prediction: bool,
) -> impl IntoElement {
ButtonLike::new("loading_button")
Button::new("loading_button", label)
.full_width()
.disabled(true)
.loading(true)
.style(ButtonStyle::Outlined)
.when(edit_prediction, |this| this.size(ButtonSize::Medium))
.child(
h_flex()
.w_full()
.gap_1()
.justify_center()
.child(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Muted)
.with_rotate_animation(4),
)
.child(Label::new(label)),
)
}
fn render_sign_in_button(&self, edit_prediction: bool) -> impl IntoElement {

View file

@ -1698,16 +1698,6 @@ impl EditPredictionStore {
..
} = pending_prediction;
let settled_editable_region_for_metrics = settled_editable_region.clone();
let kept_rate_result = cx
.background_spawn(async move {
compute_kept_rate(
&editable_region_before_prediction,
&predicted_editable_region,
&settled_editable_region_for_metrics,
)
})
.await;
#[cfg(test)]
{
let request_id = request_id.clone();
@ -1718,12 +1708,17 @@ impl EditPredictionStore {
}
});
}
cx.background_spawn({
let client = client.clone();
let llm_token = llm_token.clone();
let app_version = app_version.clone();
async move {
let kept_rate_result = compute_kept_rate(
&editable_region_before_prediction,
&predicted_editable_region,
&settled_editable_region_for_metrics,
);
let result: anyhow::Result<()> = async {
let settled_editable_region =
can_collect_data.then_some(settled_editable_region);
@ -1756,6 +1751,10 @@ impl EditPredictionStore {
model_version,
e2e_latency_ms: e2e_latency.as_millis(),
};
let json_bytes = serde_json::to_vec(&body)?;
let compressed = zstd::encode_all(&json_bytes[..], 3)?;
let url = client
.http_client()
.build_zed_llm_url("/predict_edits/settled", &[])?;
@ -1763,7 +1762,8 @@ impl EditPredictionStore {
|builder| {
Ok(builder
.uri(url.as_ref())
.body(serde_json::to_string(&body)?.into())?)
.header("Content-Encoding", "zstd")
.body(compressed.clone().into())?)
},
client,
llm_token,

View file

@ -2541,6 +2541,11 @@ fn init_test_with_fake_client_and_legacy_data_collection(
let http_client = FakeHttpClient::create({
move |req| {
let uri = req.uri().path().to_string();
let content_encoding = req
.headers()
.get("Content-Encoding")
.and_then(|value| value.to_str().ok())
.map(str::to_owned);
let mut body = req.into_body();
let predict_req_tx = predict_req_tx.clone();
let reject_req_tx = reject_req_tx.clone();
@ -2573,7 +2578,12 @@ fn init_test_with_fake_client_and_legacy_data_collection(
"/predict_edits/settled" => {
let mut buf = Vec::new();
body.read_to_end(&mut buf).await.ok();
let req = serde_json::from_slice(&buf).unwrap();
let body = if content_encoding.as_deref() == Some("zstd") {
zstd::decode_all(&buf[..]).unwrap()
} else {
buf
};
let req = serde_json::from_slice(&body).unwrap();
settled_req_tx.unbounded_send(req).unwrap();
serde_json::to_string(&SubmitEditPredictionSettledResponse {}).unwrap()
}

View file

@ -63,6 +63,7 @@ async fn run_git_blame(
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.spawn()
.context("starting git blame process")?
};

View file

@ -29,6 +29,7 @@ profiling.workspace = true
raw-window-handle = "0.6"
smallvec.workspace = true
swash = "0.2.6"
unicode-segmentation.workspace = true
gpui_util.workspace = true
wgpu.workspace = true
@ -43,4 +44,11 @@ pollster.workspace = true
wasm-bindgen.workspace = true
wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3", features = ["HtmlCanvasElement"] }
js-sys = "0.3"
js-sys = "0.3"
[dev-dependencies]
criterion.workspace = true
[[bench]]
name = "layout_line"
harness = false

View file

@ -0,0 +1,82 @@
use criterion::{Criterion, criterion_group, criterion_main};
use gpui::{FontFallbacks, FontRun, PlatformTextSystem, font, px};
use gpui_wgpu::CosmicTextSystem;
use std::borrow::Cow;
const LILEX: &[u8] = include_bytes!("../../../assets/fonts/lilex/Lilex-Regular.ttf");
const IBM_PLEX: &[u8] =
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf");
// ~4 000 chars of typical ASCII code text.
fn code_text() -> String {
concat!(
" fn compute_run_spans(\n",
" text: &str,\n",
" run_offset: usize,\n",
" run_len: usize,\n",
" primary: FontId,\n",
" fallback_chain: &[(FontId, SharedString)],\n",
" covers: &impl Fn(FontId, char) -> bool,\n",
" ) -> SmallVec<[RunSpan; 4]> {\n",
" let mut spans = SmallVec::new();\n",
" let run_end = run_offset + run_len;\n",
" if run_end <= run_offset { return spans; }\n",
" let run_text = &text[run_offset..run_end];\n",
" let mut span_start = run_offset;\n",
" let mut span_slot: Option<usize> = None;\n",
" for (ch_idx, ch) in run_text.char_indices() {\n",
" let abs = run_offset + ch_idx;\n",
" let next = pick_covering_slot(ch, span_slot, primary, fallback_chain, covers);\n",
" if next == span_slot { continue; }\n",
" if abs > span_start {\n",
" spans.push(RunSpan { start: span_start, end: abs, slot: span_slot });\n",
" }\n",
" span_start = abs;\n",
" span_slot = next;\n",
" }\n",
" spans\n",
" }\n",
)
.repeat(8) // ~3 800 chars
}
fn bench_layout_line(c: &mut Criterion) {
let system = CosmicTextSystem::new_without_system_fonts("Lilex");
system
.add_fonts(vec![Cow::Borrowed(LILEX), Cow::Borrowed(IBM_PLEX)])
.unwrap();
let font_id_no_fallback = system.font_id(&font("Lilex")).unwrap();
let font_id_with_fallback = {
let mut f = font("Lilex");
f.fallbacks = Some(FontFallbacks::from_fonts(vec!["IBM Plex Sans".to_string()]));
system.font_id(&f).unwrap()
};
let text = code_text();
let runs_no_fallback = vec![FontRun {
len: text.len(),
font_id: font_id_no_fallback,
}];
let runs_with_fallback = vec![FontRun {
len: text.len(),
font_id: font_id_with_fallback,
}];
let mut group = c.benchmark_group("layout_line");
group.bench_function("no_fallback", |b| {
b.iter(|| system.layout_line(&text, px(14.0), &runs_no_fallback))
});
group.bench_function("with_fallback_ascii", |b| {
b.iter(|| system.layout_line(&text, px(14.0), &runs_with_fallback))
});
group.finish();
}
criterion_group!(benches, bench_layout_line);
criterion_main!(benches);

File diff suppressed because it is too large Load diff

View file

@ -232,10 +232,6 @@ pub trait LanguageModel: Send + Sync {
.boxed()
}
fn cache_configuration(&self) -> Option<LanguageModelCacheConfiguration> {
None
}
#[cfg(any(test, feature = "test-support"))]
fn as_fake(&self) -> &fake_provider::FakeLanguageModel {
unimplemented!()

View file

@ -28,13 +28,6 @@ pub use crate::tool_schema::LanguageModelToolSchemaFormat;
pub use crate::util::{fix_streamed_json, parse_prompt_too_long, parse_tool_arguments};
pub use gpui_shared_string::SharedString;
#[derive(Clone, Debug)]
pub struct LanguageModelCacheConfiguration {
pub max_cache_anchors: usize,
pub should_speculate: bool,
pub min_total_token: u64,
}
/// A completion event from a language model.
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub enum LanguageModelCompletionEvent {

View file

@ -9,11 +9,10 @@ use gpui::{AnyView, App, AsyncApp, Context, Entity, Task, TaskExt};
use http_client::HttpClient;
use language_model::{
ANTHROPIC_PROVIDER_ID, ANTHROPIC_PROVIDER_NAME, ApiKeyState, AuthenticateError,
ConfigurationViewTargetAgent, EnvVar, IconOrSvg, LanguageModel,
LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelCompletionEvent,
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
LanguageModelToolChoice, RateLimiter, env_var,
ConfigurationViewTargetAgent, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, RateLimiter, env_var,
};
use settings::{Settings, SettingsStore};
use std::sync::{Arc, LazyLock};
@ -507,10 +506,6 @@ impl LanguageModel for AnthropicModel {
});
async move { Ok(future.await?.boxed()) }.boxed()
}
fn cache_configuration(&self) -> Option<LanguageModelCacheConfiguration> {
None
}
}
struct ConfigurationView {

View file

@ -32,12 +32,11 @@ use gpui::{
use gpui_tokio::Tokio;
use http_client::HttpClient;
use language_model::{
AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCacheConfiguration,
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
LanguageModelToolResultContent, LanguageModelToolUse, MessageContent, RateLimiter, Role,
TokenUsage, env_var,
AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
LanguageModelToolUse, MessageContent, RateLimiter, Role, TokenUsage, env_var,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@ -799,16 +798,6 @@ impl LanguageModel for BedrockModel {
async move { Ok(future.await?.boxed()) }.boxed()
}
fn cache_configuration(&self) -> Option<LanguageModelCacheConfiguration> {
self.model
.cache_configuration()
.map(|config| LanguageModelCacheConfiguration {
max_cache_anchors: config.max_cache_anchors,
should_speculate: false,
min_total_token: config.min_total_token,
})
}
}
fn deny_tool_use_events(

View file

@ -1015,6 +1015,7 @@ impl Render for ConfigurationView {
.unwrap_or_else(|| "Signed in".to_string());
let weak_state = self.state.downgrade();
return v_flex()
.child(
ConfiguredApiCard::new(SharedString::from(label))
@ -1026,30 +1027,52 @@ impl Render for ConfigurationView {
.into_any_element();
}
if state.is_signing_in() {
return v_flex()
.child(Label::new("Signing in…").color(Color::Muted))
.into_any_element();
}
let last_auth_error = state.last_auth_error.clone();
let provider_state = self.state.clone();
let http_client = self.http_client.clone();
let is_signing_in = state.is_signing_in();
let button_label = if is_signing_in {
"Signing in…"
} else {
"Sign in to use ChatGPT Subscription"
};
v_flex()
.gap_2()
.when_some(last_auth_error, |this, error| {
this.child(Label::new(error).color(Color::Error))
})
.child(Label::new(
"Sign in with your ChatGPT Plus or Pro subscription to use OpenAI models in Zed's agent.",
))
.child(
Button::new("sign-in", "Sign in with ChatGPT")
Button::new("sign-in", button_label)
.full_width()
.style(ButtonStyle::Outlined)
.loading(is_signing_in)
.disabled(is_signing_in)
.when(!is_signing_in, |this| {
this.start_icon(
Icon::new(IconName::AiOpenAi)
.size(IconSize::Small)
.color(Color::Muted),
)
})
.on_click(move |_, _window, cx| {
do_sign_in(&provider_state, &http_client, cx);
}),
)
.when_some(last_auth_error, |this, error| {
this.child(
h_flex()
.gap_1()
.justify_center()
.child(
Icon::new(IconName::XCircle)
.color(Color::Error)
.size(IconSize::Small),
)
.child(Label::new(error).color(Color::Muted)),
)
})
.into_any_element()
}
}

View file

@ -20,12 +20,12 @@ use http_client::{
};
use language_model::{
ANTHROPIC_PROVIDER_ID, ANTHROPIC_PROVIDER_NAME, GOOGLE_PROVIDER_ID, GOOGLE_PROVIDER_NAME,
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelEffortLevel, LanguageModelId, LanguageModelName,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelRequest,
LanguageModelToolChoice, LanguageModelToolSchemaFormat, OPEN_AI_PROVIDER_ID,
OPEN_AI_PROVIDER_NAME, PaymentRequiredError, RateLimiter, X_AI_PROVIDER_ID, X_AI_PROVIDER_NAME,
ZED_CLOUD_PROVIDER_ID, ZED_CLOUD_PROVIDER_NAME,
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
LanguageModelEffortLevel, LanguageModelId, LanguageModelName, LanguageModelProviderId,
LanguageModelProviderName, LanguageModelRequest, LanguageModelToolChoice,
LanguageModelToolSchemaFormat, OPEN_AI_PROVIDER_ID, OPEN_AI_PROVIDER_NAME,
PaymentRequiredError, RateLimiter, X_AI_PROVIDER_ID, X_AI_PROVIDER_NAME, ZED_CLOUD_PROVIDER_ID,
ZED_CLOUD_PROVIDER_NAME,
};
use schemars::JsonSchema;
@ -368,21 +368,6 @@ impl<TP: CloudLlmTokenProvider + 'static> LanguageModel for CloudLanguageModel<T
Some(self.model.max_output_tokens as u64)
}
fn cache_configuration(&self) -> Option<LanguageModelCacheConfiguration> {
match &self.model.provider {
cloud_llm_client::LanguageModelProvider::Anthropic => {
Some(LanguageModelCacheConfiguration {
min_total_token: 2_048,
should_speculate: true,
max_cache_anchors: 4,
})
}
cloud_llm_client::LanguageModelProvider::OpenAi
| cloud_llm_client::LanguageModelProvider::XAi
| cloud_llm_client::LanguageModelProvider::Google => None,
}
}
fn stream_completion(
&self,
request: LanguageModelRequest,

View file

@ -23,6 +23,7 @@ Zed supports these providers with your own API keys:
- [Amazon Bedrock](#amazon-bedrock)
- [Anthropic](#anthropic)
- [ChatGPT Subscription](#chatgpt-subscription)
- [DeepSeek](#deepseek)
- [GitHub Copilot Chat](#github-copilot-chat)
- [Google AI](#google-ai)
@ -225,6 +226,18 @@ You can configure a model to use [extended thinking](https://docs.anthropic.com/
}
```
### ChatGPT Subscription {#chatgpt-subscription}
Use your existing ChatGPT Plus or Pro subscription to access OpenAI models directly in Zed — no separate API key required.
1. Open the settings view ({#action agent::OpenSettings}) and go to the ChatGPT Subscription section
2. Click **Sign in** and complete the OpenAI authentication in your browser
3. Once signed in, models appear in the model dropdown, including GPT-5.5 and GPT-5.3 Codex
To sign out, click **Sign Out** in the ChatGPT Subscription settings.
> **Note:** Model availability depends on your ChatGPT subscription tier. Some models may require ChatGPT Pro.
### DeepSeek {#deepseek}
1. Visit the DeepSeek platform and [create an API key](https://platform.deepseek.com/api_keys)

View file

@ -86,6 +86,7 @@ fn docs_build_steps(
steps::use_clang(
job.add_env(("DOCS_AMPLITUDE_API_KEY", vars::DOCS_AMPLITUDE_API_KEY))
.add_env(("DOCS_CONSENT_IO_INSTANCE", vars::DOCS_CONSENT_IO_INSTANCE))
.add_step(
steps::checkout_repo().when_some(checkout_ref, |step, checkout_ref| {
step.with_ref(checkout_ref)
@ -269,6 +270,10 @@ pub(crate) fn deploy_docs_workflow_call(
"DOCS_AMPLITUDE_API_KEY".to_owned(),
vars::DOCS_AMPLITUDE_API_KEY.to_owned(),
),
(
"DOCS_CONSENT_IO_INSTANCE".to_owned(),
vars::DOCS_CONSENT_IO_INSTANCE.to_owned(),
),
(
"CLOUDFLARE_API_TOKEN".to_owned(),
vars::CLOUDFLARE_API_TOKEN.to_owned(),
@ -327,6 +332,13 @@ pub(crate) fn deploy_docs() -> Workflow {
required: true,
},
),
(
"DOCS_CONSENT_IO_INSTANCE".to_owned(),
WorkflowCallSecret {
description: "DOCS_CONSENT_IO_INSTANCE".to_owned(),
required: true,
},
),
(
"CLOUDFLARE_API_TOKEN".to_owned(),
WorkflowCallSecret {

View file

@ -54,6 +54,7 @@ secret!(R2_SECRET_ACCESS_KEY);
secret!(CLOUDFLARE_API_TOKEN);
secret!(CLOUDFLARE_ACCOUNT_ID);
secret!(DOCS_AMPLITUDE_API_KEY);
secret!(DOCS_CONSENT_IO_INSTANCE);
// todo(ci) make these secrets too...
var!(AZURE_SIGNING_ACCOUNT_NAME);