Compare commits

...

10 Commits

Author SHA1 Message Date
Danilo Leal
5ac874ab31 Agent panel message editing checkpoint
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-07-24 13:59:57 -03:00
Kirill Bulatov
2658b2801e Shutdown language servers better (#35038)
Follow-up of https://github.com/zed-industries/zed/pull/33417

* adjust prettier mock LSP to handle `shutdown` and `exit` messages
* removed another `?.log_err()` backtrace from logs and improved the
logging info
* always handle the last parts of the shutdown logic even if the
shutdown response had failed

Release Notes:

- N/A
2025-07-24 15:24:53 +00:00
Richard Feldman
2a9355a3d2 Don't auto-retry in certain circumstances (#35037)
Someone encountered this in production, which should not happen:

<img width="1266" height="623" alt="Screenshot 2025-07-24 at 10 38
40 AM"
src="https://github.com/user-attachments/assets/40f3f977-5110-4808-a456-7e708d953b3b"
/>

This moves certain errors into the category of "never retry" and reduces
the number of retries for some others. Also it adds some diagnostic
logging for retry policy.

It's not a complete fix for the above, because the underlying issues is
that the server is sending a HTTP 403 response and although we were
already treating 403s as "do not retry" it was deciding to retry with 2
attempts anyway. So further debugging is needed to figure out why it
wasn't going down the 403 branch by the time the request got here.

Release Notes:

- N/A
2025-07-24 11:11:26 -04:00
Smit Barmase
fa788a39a4 project_panel: Reuse index_for_entry in index_for_selection (#35034)
Just refactor I came across while working on another issue.
`index_for_entry` and `index_for_selection` have the exact same logic,
here we can simply reuse `index_for_entry` for `index_for_selection`.

Release Notes:

- N/A
2025-07-24 20:07:38 +05:30
Pablo Ramón Guevara
7cdd808db2 helix: Fix replace in helix mode (#34789)
Closes https://github.com/zed-industries/zed/issues/33076

Release Notes:

- Fixed replace command on helix mode: now it actually replaces what was
selected and keeps the replaced text selected to better match helix
2025-07-24 08:29:58 -06:00
Danilo Leal
29332c1962 ai onboarding: Add overall fixes to the whole flow (#34996)
Closes https://github.com/zed-industries/zed/issues/34979

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Ben Kunkle <Ben.kunkle@gmail.com>
2025-07-24 11:26:15 -03:00
Peter Tripp
fab450e39d Fix invalid regular expressions highlighting all search fields (#35001)
Closes https://github.com/zed-industries/zed/issues/34969
Closes https://github.com/zed-industries/zed/issues/34970

Only highlight the search field on regex error (buffer search and
project search).
Clear errors when the buffer search hidden so stale errors aren't shown
on next search.

Before (all fields highlighted red): 
<img width="631" height="180" alt="Screenshot 2025-07-23 at 22 59 45"
src="https://github.com/user-attachments/assets/a91d3090-1bae-4718-a1f5-0a1237559ff2"
/>

After (only query field highlighted red): 
<img width="632" height="187" alt="Screenshot 2025-07-23 at 23 10 49"
src="https://github.com/user-attachments/assets/6ae72c84-9333-4cdb-907b-afb7a0a6e808"
/>

Release Notes:

- Improved highlighting of regex errors in search dialogs
2025-07-24 09:20:25 -04:00
Danilo Leal
4fb540d6d2 dock: Add divider between panels on the right side, too (#35003)
A while ago, we added a divider in the left side of the status bar
between the icon buttons that open panels vs. those that open something
else (tabs, popover menus, etc.). There isn't a super good reason why we
wouldn't do the same in the right side, even more so given that (at
least by default) we usually have more buttons on that side; buttons
that _don't_ open panels (regardless of being docked to the bottom or
right). So, this PR does this!

<img width="700" height="314" alt="CleanShot 2025-07-24 at 1  59 10@2x"
src="https://github.com/user-attachments/assets/5f8bd4bc-a983-4000-a8f9-05a36b9e4b81"
/>

Release Notes:

- N/A
2025-07-24 09:39:10 -03:00
Piotr Osiewicz
1e2b0fcab6 sum_tree: Remove Unit type (#35027)
This solves one ~TODO, as Unit type was a workaround for a lack of
ability to implement Summary for ().


Release Notes:

- N/A
2025-07-24 14:30:49 +02:00
Oleksiy Syvokon
0af690080b linux: Fix ctrl-0..9, ctrl-[, ctrl-^ (#35028)
There were two different underlying reasons for the issues with
ctrl-number and ctrl-punctuation:

1. Some keys in the ctrl-0..9 range send codes in the `\1b`..`\1f`
range. For example, `ctrl-2` sends keycode for `ctrl-[` (0x1b), but we
want to map it to `2`, not to `[`.

2. `ctrl-[` and four other ctrl-punctuation were incorrectly mapped,
since the expected conversion is by adding 0x40

Closes #35012

Release Notes:

- N/A
2025-07-24 12:22:57 +00:00
29 changed files with 1109 additions and 444 deletions

1
Cargo.lock generated
View File

@@ -249,6 +249,7 @@ dependencies = [
"prompt_store",
"proto",
"rand 0.8.5",
"regex",
"release_channel",
"rope",
"rules_library",

View File

@@ -2037,6 +2037,12 @@ impl Thread {
if let Some(retry_strategy) =
Thread::get_retry_strategy(completion_error)
{
log::info!(
"Retrying with {:?} for language model completion error {:?}",
retry_strategy,
completion_error
);
retry_scheduled = thread
.handle_retryable_error_with_delay(
&completion_error,
@@ -2246,15 +2252,14 @@ impl Thread {
..
}
| AuthenticationError { .. }
| PermissionError { .. } => None,
// These errors might be transient, so retry them
SerializeRequest { .. }
| BuildRequestBody { .. }
| PromptTooLarge { .. }
| PermissionError { .. }
| NoApiKey { .. }
| ApiEndpointNotFound { .. }
| NoApiKey { .. } => Some(RetryStrategy::Fixed {
| PromptTooLarge { .. } => None,
// These errors might be transient, so retry them
SerializeRequest { .. } | BuildRequestBody { .. } => Some(RetryStrategy::Fixed {
delay: BASE_RETRY_DELAY,
max_attempts: 2,
max_attempts: 1,
}),
// Retry all other 4xx and 5xx errors once.
HttpResponseError { status_code, .. }

View File

@@ -41,6 +41,9 @@ use std::{
};
use util::ResultExt as _;
pub static ZED_STATELESS: std::sync::LazyLock<bool> =
std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()));
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DataType {
#[serde(rename = "json")]
@@ -874,7 +877,11 @@ impl ThreadsDatabase {
let needs_migration_from_heed = mdb_path.exists();
let connection = Connection::open_file(&sqlite_path.to_string_lossy());
let connection = if *ZED_STATELESS {
Connection::open_memory(Some("THREAD_FALLBACK_DB"))
} else {
Connection::open_file(&sqlite_path.to_string_lossy())
};
connection.exec(indoc! {"
CREATE TABLE IF NOT EXISTS threads (

View File

@@ -68,6 +68,7 @@ picker.workspace = true
project.workspace = true
prompt_store.workspace = true
proto.workspace = true
regex.workspace = true
release_channel.workspace = true
rope.workspace = true
rules_library.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -185,6 +185,13 @@ impl AgentConfiguration {
None
};
let is_signed_in = self
.workspace
.read_with(cx, |workspace, _| {
workspace.client().status().borrow().is_connected()
})
.unwrap_or(false);
v_flex()
.w_full()
.when(is_expanded, |this| this.mb_2())
@@ -233,8 +240,8 @@ impl AgentConfiguration {
.size(LabelSize::Large),
)
.map(|this| {
if is_zed_provider {
this.gap_2().child(
if is_zed_provider && is_signed_in {
this.child(
self.render_zed_plan_info(current_plan, cx),
)
} else {

View File

@@ -9,7 +9,6 @@ use agent_servers::AgentServer;
use db::kvp::{Dismissable, KEY_VALUE_STORE};
use serde::{Deserialize, Serialize};
use crate::NewExternalAgentThread;
use crate::agent_diff::AgentDiffThread;
use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
use crate::ui::NewThreadButton;
@@ -31,6 +30,7 @@ use crate::{
thread_history::{HistoryEntryElement, ThreadHistory},
ui::{AgentOnboardingModal, EndTrialUpsell},
};
use crate::{EditAssistantMessage, EditUserMessage, NewExternalAgentThread};
use agent::{
Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio,
context_store::ContextStore,
@@ -564,6 +564,17 @@ impl AgentPanel {
let inline_assist_context_store =
cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
let thread_id = thread.read(cx).id().clone();
let history_store = cx.new(|cx| {
HistoryStore::new(
thread_store.clone(),
context_store.clone(),
[HistoryEntryId::Thread(thread_id)],
cx,
)
});
let message_editor = cx.new(|cx| {
MessageEditor::new(
fs.clone(),
@@ -573,22 +584,13 @@ impl AgentPanel {
prompt_store.clone(),
thread_store.downgrade(),
context_store.downgrade(),
Some(history_store.downgrade()),
thread.clone(),
window,
cx,
)
});
let thread_id = thread.read(cx).id().clone();
let history_store = cx.new(|cx| {
HistoryStore::new(
thread_store.clone(),
context_store.clone(),
[HistoryEntryId::Thread(thread_id)],
cx,
)
});
cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
let active_thread = cx.new(|cx| {
@@ -851,6 +853,7 @@ impl AgentPanel {
self.prompt_store.clone(),
self.thread_store.downgrade(),
self.context_store.downgrade(),
Some(self.history_store.downgrade()),
thread.clone(),
window,
cx,
@@ -1124,6 +1127,7 @@ impl AgentPanel {
self.prompt_store.clone(),
self.thread_store.downgrade(),
self.context_store.downgrade(),
Some(self.history_store.downgrade()),
thread.clone(),
window,
cx,
@@ -2283,20 +2287,21 @@ impl AgentPanel {
}
match &self.active_view {
ActiveView::Thread { thread, .. } => thread
.read(cx)
.thread()
.read(cx)
.configured_model()
.map_or(true, |model| {
model.provider.id() == language_model::ZED_CLOUD_PROVIDER_ID
}),
ActiveView::TextThread { .. } => LanguageModelRegistry::global(cx)
.read(cx)
.default_model()
.map_or(true, |model| {
model.provider.id() == language_model::ZED_CLOUD_PROVIDER_ID
}),
ActiveView::Thread { .. } | ActiveView::TextThread { .. } => {
let history_is_empty = self
.history_store
.update(cx, |store, cx| store.recent_entries(1, cx).is_empty());
let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
.providers()
.iter()
.any(|provider| {
provider.is_authenticated(cx)
&& provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
});
history_is_empty || !has_configured_non_zed_providers
}
ActiveView::ExternalAgentThread { .. }
| ActiveView::History
| ActiveView::Configuration => false,
@@ -2317,9 +2322,8 @@ impl AgentPanel {
Some(
div()
.size_full()
.when(thread_view, |this| {
this.bg(cx.theme().colors().panel_background)
this.size_full().bg(cx.theme().colors().panel_background)
})
.when(text_thread_view, |this| {
this.bg(cx.theme().colors().editor_background)
@@ -3218,6 +3222,20 @@ impl Render for AgentPanel {
}
}))
.on_action(cx.listener(Self::toggle_burn_mode))
// .on_action(cx.listener(|this, _: &EditAssistantMessage, window, cx| {
// if let ActiveView::Thread { thread, .. } = &this.active_view {
// thread.update(cx, |this, cx| {
// this.edit_last_message(Role::Assistant, window, cx);
// });
// }
// }))
// .on_action(cx.listener(|this, _: &EditUserMessage, window, cx| {
// if let ActiveView::Thread { thread, .. } = &this.active_view {
// thread.update(cx, |this, cx| {
// this.edit_last_message(Role::User, window, cx);
// });
// }
// }))
.child(self.render_toolbar(window, cx))
.children(self.render_onboarding(window, cx))
.children(self.render_trial_end_upsell(window, cx))

View File

@@ -123,6 +123,8 @@ actions!(
ContinueWithBurnMode,
/// Toggles burn mode for faster responses.
ToggleBurnMode,
EditAssistantMessage,
EditUserMessage,
]
);

View File

@@ -9,6 +9,7 @@ use crate::ui::{
MaxModeTooltip,
preview::{AgentPreview, UsageCallout},
};
use agent::history_store::HistoryStore;
use agent::{
context::{AgentContextKey, ContextLoadResult, load_context},
context_store::ContextStoreEvent,
@@ -29,8 +30,9 @@ use fs::Fs;
use futures::future::Shared;
use futures::{FutureExt as _, future};
use gpui::{
Animation, AnimationExt, App, Entity, EventEmitter, Focusable, KeyContext, Subscription, Task,
TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
Animation, AnimationExt, App, Entity, EventEmitter, Focusable, IntoElement, KeyContext,
Subscription, Task, TextStyle, WeakEntity, linear_color_stop, linear_gradient, point,
pulsating_between,
};
use language::{Buffer, Language, Point};
use language_model::{
@@ -80,6 +82,7 @@ pub struct MessageEditor {
user_store: Entity<UserStore>,
context_store: Entity<ContextStore>,
prompt_store: Option<Entity<PromptStore>>,
history_store: Option<WeakEntity<HistoryStore>>,
context_strip: Entity<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Entity<AgentModelSelector>,
@@ -161,6 +164,7 @@ impl MessageEditor {
prompt_store: Option<Entity<PromptStore>>,
thread_store: WeakEntity<ThreadStore>,
text_thread_store: WeakEntity<TextThreadStore>,
history_store: Option<WeakEntity<HistoryStore>>,
thread: Entity<Thread>,
window: &mut Window,
cx: &mut Context<Self>,
@@ -233,6 +237,7 @@ impl MessageEditor {
workspace,
context_store,
prompt_store,
history_store,
context_strip,
context_picker_menu_handle,
load_context_task: None,
@@ -1661,32 +1666,36 @@ impl Render for MessageEditor {
let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5;
let in_pro_trial = matches!(
self.user_store.read(cx).current_plan(),
Some(proto::Plan::ZedProTrial)
);
let has_configured_providers = LanguageModelRegistry::read_global(cx)
.providers()
.iter()
.filter(|provider| {
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
})
.count()
> 0;
let pro_user = matches!(
self.user_store.read(cx).current_plan(),
Some(proto::Plan::ZedPro)
);
let is_signed_out = self
.workspace
.read_with(cx, |workspace, _| {
workspace.client().status().borrow().is_signed_out()
})
.unwrap_or(true);
let configured_providers: Vec<(IconName, SharedString)> =
LanguageModelRegistry::read_global(cx)
.providers()
.iter()
.filter(|provider| {
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
})
.map(|provider| (provider.icon(), provider.name().0.clone()))
.collect();
let has_existing_providers = configured_providers.len() > 0;
let has_history = self
.history_store
.as_ref()
.and_then(|hs| hs.update(cx, |hs, cx| hs.entries(cx).len() > 0).ok())
.unwrap_or(false)
|| self
.thread
.read_with(cx, |thread, _| thread.messages().len() > 0);
v_flex()
.size_full()
.bg(cx.theme().colors().panel_background)
.when(
has_existing_providers && !in_pro_trial && !pro_user,
!has_history && is_signed_out && has_configured_providers,
|this| this.child(cx.new(ApiKeysWithProviders::new)),
)
.when(changed_buffers.len() > 0, |parent| {
@@ -1778,6 +1787,7 @@ impl AgentPreview for MessageEditor {
None,
thread_store.downgrade(),
text_thread_store.downgrade(),
None,
thread,
window,
cx,

View File

@@ -5,7 +5,6 @@ mod end_trial_upsell;
mod new_thread_button;
mod onboarding_modal;
pub mod preview;
mod upsell;
pub use agent_notification::*;
pub use burn_mode_tooltip::*;

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use ai_onboarding::{AgentPanelOnboardingCard, BulletItem};
use client::zed_urls;
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
use ui::{Divider, List, prelude::*};
use ui::{Divider, List, Tooltip, prelude::*};
#[derive(IntoElement, RegisterComponent)]
pub struct EndTrialUpsell {
@@ -33,14 +33,19 @@ impl RenderOnce for EndTrialUpsell {
)
.child(
List::new()
.child(BulletItem::new("500 prompts per month with Claude models"))
.child(BulletItem::new("Unlimited edit predictions")),
.child(BulletItem::new("500 prompts with Claude models"))
.child(BulletItem::new(
"Unlimited edit predictions with Zeta, our open-source model",
)),
)
.child(
Button::new("cta-button", "Upgrade to Zed Pro")
.full_width()
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))),
.on_click(move |_, _window, cx| {
telemetry::event!("Upgrade To Pro Clicked", state = "end-of-trial");
cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
}),
);
let free_section = v_flex()
@@ -55,37 +60,43 @@ impl RenderOnce for EndTrialUpsell {
.color(Color::Muted)
.buffer_font(cx),
)
.child(
Label::new("(Current Plan)")
.size(LabelSize::Small)
.color(Color::Custom(cx.theme().colors().text_muted.opacity(0.6)))
.buffer_font(cx),
)
.child(Divider::horizontal()),
)
.child(
List::new()
.child(BulletItem::new(
"50 prompts per month with the Claude models",
))
.child(BulletItem::new(
"2000 accepted edit predictions using our open-source Zeta model",
)),
)
.child(
Button::new("dismiss-button", "Stay on Free")
.full_width()
.style(ButtonStyle::Outlined)
.on_click({
let callback = self.dismiss_upsell.clone();
move |_, window, cx| callback(window, cx)
}),
.child(BulletItem::new("50 prompts with the Claude models"))
.child(BulletItem::new("2,000 accepted edit predictions")),
);
AgentPanelOnboardingCard::new()
.child(Headline::new("Your Zed Pro trial has expired."))
.child(Headline::new("Your Zed Pro Trial has expired"))
.child(
Label::new("You've been automatically reset to the Free plan.")
.size(LabelSize::Small)
.color(Color::Muted)
.mb_1(),
.mb_2(),
)
.child(pro_section)
.child(free_section)
.child(
h_flex().absolute().top_4().right_4().child(
IconButton::new("dismiss_onboarding", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss"))
.on_click({
let callback = self.dismiss_upsell.clone();
move |_, window, cx| {
telemetry::event!("Banner Dismissed", source = "AI Onboarding");
callback(window, cx)
}
}),
),
)
}
}

View File

@@ -1,163 +0,0 @@
use component::{Component, ComponentScope, single_example};
use gpui::{
AnyElement, App, ClickEvent, IntoElement, ParentElement, RenderOnce, SharedString, Styled,
Window,
};
use theme::ActiveTheme;
use ui::{
Button, ButtonCommon, ButtonStyle, Checkbox, Clickable, Color, Label, LabelCommon,
RegisterComponent, ToggleState, h_flex, v_flex,
};
/// A component that displays an upsell message with a call-to-action button
///
/// # Example
/// ```
/// let upsell = Upsell::new(
/// "Upgrade to Zed Pro",
/// "Get access to advanced AI features and more",
/// "Upgrade Now",
/// Box::new(|_, _window, cx| {
/// cx.open_url("https://zed.dev/pricing");
/// }),
/// Box::new(|_, _window, cx| {
/// // Handle dismiss
/// }),
/// Box::new(|checked, window, cx| {
/// // Handle don't show again
/// }),
/// );
/// ```
#[derive(IntoElement, RegisterComponent)]
pub struct Upsell {
title: SharedString,
message: SharedString,
cta_text: SharedString,
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
on_dismiss: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
on_dont_show_again: Box<dyn Fn(bool, &mut Window, &mut App) + 'static>,
}
impl Upsell {
/// Create a new upsell component
pub fn new(
title: impl Into<SharedString>,
message: impl Into<SharedString>,
cta_text: impl Into<SharedString>,
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
on_dismiss: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
on_dont_show_again: Box<dyn Fn(bool, &mut Window, &mut App) + 'static>,
) -> Self {
Self {
title: title.into(),
message: message.into(),
cta_text: cta_text.into(),
on_click,
on_dismiss,
on_dont_show_again,
}
}
}
impl RenderOnce for Upsell {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
.w_full()
.p_4()
.gap_3()
.bg(cx.theme().colors().surface_background)
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.child(
v_flex()
.gap_1()
.child(
Label::new(self.title)
.size(ui::LabelSize::Large)
.weight(gpui::FontWeight::BOLD),
)
.child(Label::new(self.message).color(Color::Muted)),
)
.child(
h_flex()
.w_full()
.justify_between()
.items_center()
.child(
h_flex()
.items_center()
.gap_1()
.child(
Checkbox::new("dont-show-again", ToggleState::Unselected).on_click(
move |_, window, cx| {
(self.on_dont_show_again)(true, window, cx);
},
),
)
.child(
Label::new("Don't show again")
.color(Color::Muted)
.size(ui::LabelSize::Small),
),
)
.child(
h_flex()
.gap_2()
.child(
Button::new("dismiss-button", "No Thanks")
.style(ButtonStyle::Subtle)
.on_click(self.on_dismiss),
)
.child(
Button::new("cta-button", self.cta_text)
.style(ButtonStyle::Filled)
.on_click(self.on_click),
),
),
)
}
}
impl Component for Upsell {
fn scope() -> ComponentScope {
ComponentScope::Agent
}
fn name() -> &'static str {
"Upsell"
}
fn description() -> Option<&'static str> {
Some("A promotional component that displays a message with a call-to-action.")
}
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let examples = vec![
single_example(
"Default",
Upsell::new(
"Upgrade to Zed Pro",
"Get unlimited access to AI features and more with Zed Pro. Unlock advanced AI capabilities and other premium features.",
"Upgrade Now",
Box::new(|_, _, _| {}),
Box::new(|_, _, _| {}),
Box::new(|_, _, _| {}),
).render(window, cx).into_any_element(),
),
single_example(
"Short Message",
Upsell::new(
"Try Zed Pro for free",
"Start your 7-day trial today.",
"Start Trial",
Box::new(|_, _, _| {}),
Box::new(|_, _, _| {}),
Box::new(|_, _, _| {}),
).render(window, cx).into_any_element(),
),
];
Some(v_flex().gap_4().children(examples).into_any_element())
}
}

View File

@@ -61,6 +61,11 @@ impl Render for AgentPanelOnboarding {
Some(proto::Plan::ZedProTrial)
);
let is_pro_user = matches!(
self.user_store.read(cx).current_plan(),
Some(proto::Plan::ZedPro)
);
AgentPanelOnboardingCard::new()
.child(
ZedAiOnboarding::new(
@@ -75,7 +80,7 @@ impl Render for AgentPanelOnboarding {
}),
)
.map(|this| {
if enrolled_in_trial || self.configured_providers.len() >= 1 {
if enrolled_in_trial || is_pro_user || self.configured_providers.len() >= 1 {
this
} else {
this.child(ApiKeysWithoutProviders::new())

View File

@@ -16,6 +16,7 @@ use client::{Client, UserStore, zed_urls};
use gpui::{AnyElement, Entity, IntoElement, ParentElement, SharedString};
use ui::{Divider, List, ListItem, RegisterComponent, TintColor, Tooltip, prelude::*};
#[derive(IntoElement)]
pub struct BulletItem {
label: SharedString,
}
@@ -28,18 +29,27 @@ impl BulletItem {
}
}
impl IntoElement for BulletItem {
type Element = AnyElement;
impl RenderOnce for BulletItem {
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
let line_height = 0.85 * window.line_height();
fn into_element(self) -> Self::Element {
ListItem::new("list-item")
.selectable(false)
.start_slot(
Icon::new(IconName::Dash)
.size(IconSize::XSmall)
.color(Color::Hidden),
.child(
h_flex()
.w_full()
.min_w_0()
.gap_1()
.items_start()
.child(
h_flex().h(line_height).justify_center().child(
Icon::new(IconName::Dash)
.size(IconSize::XSmall)
.color(Color::Hidden),
),
)
.child(div().w_full().min_w_0().child(Label::new(self.label))),
)
.child(div().w_full().child(Label::new(self.label)))
.into_any_element()
}
}
@@ -373,7 +383,9 @@ impl ZedAiOnboarding {
.child(
List::new()
.child(BulletItem::new("500 prompts with Claude models"))
.child(BulletItem::new("Unlimited edit predictions")),
.child(BulletItem::new(
"Unlimited edit predictions with Zeta, our open-source model",
)),
)
.child(
Button::new("pro", "Continue with Zed Pro")

View File

@@ -767,6 +767,11 @@ impl ContextStore {
fn reload(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
let fs = self.fs.clone();
cx.spawn(async move |this, cx| {
pub static ZED_STATELESS: LazyLock<bool> =
LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()));
if *ZED_STATELESS {
return Ok(());
}
fs.create_dir(contexts_dir()).await?;
let mut paths = fs.read_dir(contexts_dir()).await?;

View File

@@ -765,12 +765,14 @@ impl UserStore {
pub fn current_plan(&self) -> Option<proto::Plan> {
#[cfg(debug_assertions)]
if let Ok(plan) = std::env::var("ZED_SIMULATE_ZED_PRO_PLAN").as_ref() {
if let Ok(plan) = std::env::var("ZED_SIMULATE_PLAN").as_ref() {
return match plan.as_str() {
"free" => Some(proto::Plan::Free),
"trial" => Some(proto::Plan::ZedProTrial),
"pro" => Some(proto::Plan::ZedPro),
_ => None,
_ => {
panic!("ZED_SIMULATE_PLAN must be one of 'free', 'trial', or 'pro'");
}
};
}

View File

@@ -845,9 +845,15 @@ impl crate::Keystroke {
{
if key.is_ascii_graphic() {
key_utf8.to_lowercase()
// map ctrl-a to a
} else if key_utf32 <= 0x1f {
((key_utf32 as u8 + 0x60) as char).to_string()
// map ctrl-a to `a`
// ctrl-0..9 may emit control codes like ctrl-[, but
// we don't want to map them to `[`
} else if key_utf32 <= 0x1f
&& !name.chars().next().is_some_and(|c| c.is_ascii_digit())
{
((key_utf32 as u8 + 0x40) as char)
.to_ascii_lowercase()
.to_string()
} else {
name
}

View File

@@ -1159,19 +1159,20 @@ impl RenderOnce for ZedAiConfiguration {
let manage_subscription_buttons = if is_pro {
Button::new("manage_settings", "Manage Subscription")
.full_width()
.style(ButtonStyle::Tinted(TintColor::Accent))
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx)))
.into_any_element()
} else if self.plan.is_none() || self.eligible_for_trial {
Button::new("start_trial", "Start 14-day Free Pro Trial")
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.full_width()
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(|_, _, cx| cx.open_url(&zed_urls::start_trial_url(cx)))
.into_any_element()
} else {
Button::new("upgrade", "Upgrade to Pro")
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.full_width()
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)))
.into_any_element()
};

View File

@@ -877,39 +877,41 @@ impl LanguageServer {
let server = self.server.clone();
let name = self.name.clone();
let server_id = self.server_id;
let mut timer = self.executor.timer(SERVER_SHUTDOWN_TIMEOUT).fuse();
Some(
async move {
log::debug!("language server shutdown started");
Some(async move {
log::debug!("language server shutdown started");
select! {
request_result = shutdown_request.fuse() => {
match request_result {
ConnectionResult::Timeout => {
log::warn!("timeout waiting for language server {name} to shutdown");
},
ConnectionResult::ConnectionReset => {},
ConnectionResult::Result(r) => r?,
}
select! {
request_result = shutdown_request.fuse() => {
match request_result {
ConnectionResult::Timeout => {
log::warn!("timeout waiting for language server {name} (id {server_id}) to shutdown");
},
ConnectionResult::ConnectionReset => {
log::warn!("language server {name} (id {server_id}) closed the shutdown request connection");
},
ConnectionResult::Result(Err(e)) => {
log::error!("Shutdown request failure, server {name} (id {server_id}): {e:#}");
},
ConnectionResult::Result(Ok(())) => {}
}
_ = timer => {
log::info!("timeout waiting for language server {name} to shutdown");
},
}
response_handlers.lock().take();
Self::notify_internal::<notification::Exit>(&outbound_tx, &()).ok();
outbound_tx.close();
output_done.recv().await;
server.lock().take().map(|mut child| child.kill());
log::debug!("language server shutdown finished");
drop(tasks);
anyhow::Ok(())
_ = timer => {
log::info!("timeout waiting for language server {name} (id {server_id}) to shutdown");
},
}
.log_err(),
)
response_handlers.lock().take();
Self::notify_internal::<notification::Exit>(&outbound_tx, &()).ok();
outbound_tx.close();
output_done.recv().await;
server.lock().take().map(|mut child| child.kill());
drop(tasks);
log::debug!("language server shutdown finished");
Some(())
})
} else {
None
}

View File

@@ -16,6 +16,7 @@ actions!(
Cancel,
/// Confirms the selected menu item.
Confirm,
SaveEdit,
/// Performs secondary confirmation action.
SecondaryConfirm,
/// Selects the previous item in the menu.

View File

@@ -152,6 +152,10 @@ async function handleMessage(message, prettier) {
throw new Error(`Message method is undefined: ${JSON.stringify(message)}`);
} else if (method == "initialized") {
return;
} else if (method === "shutdown") {
sendResponse({ result: {} });
} else if (method == "exit") {
process.exit(0);
}
if (id === undefined) {

View File

@@ -2723,26 +2723,7 @@ impl ProjectPanel {
}
fn index_for_selection(&self, selection: SelectedEntry) -> Option<(usize, usize, usize)> {
let mut entry_index = 0;
let mut visible_entries_index = 0;
for (worktree_index, (worktree_id, worktree_entries, _)) in
self.visible_entries.iter().enumerate()
{
if *worktree_id == selection.worktree_id {
for entry in worktree_entries {
if entry.id == selection.entry_id {
return Some((worktree_index, entry_index, visible_entries_index));
} else {
visible_entries_index += 1;
entry_index += 1;
}
}
break;
} else {
visible_entries_index += worktree_entries.len();
}
}
None
self.index_for_entry(selection.entry_id, selection.worktree_id)
}
fn disjoint_entries(&self, cx: &App) -> BTreeSet<SelectedEntry> {
@@ -3353,12 +3334,12 @@ impl ProjectPanel {
entry_id: ProjectEntryId,
worktree_id: WorktreeId,
) -> Option<(usize, usize, usize)> {
let mut worktree_ix = 0;
let mut total_ix = 0;
for (current_worktree_id, visible_worktree_entries, _) in &self.visible_entries {
for (worktree_ix, (current_worktree_id, visible_worktree_entries, _)) in
self.visible_entries.iter().enumerate()
{
if worktree_id != *current_worktree_id {
total_ix += visible_worktree_entries.len();
worktree_ix += 1;
continue;
}

View File

@@ -228,16 +228,17 @@ impl Render for BufferSearchBar {
if in_replace {
key_context.add("in_replace");
}
let editor_border = if self.query_error.is_some() {
let query_border = if self.query_error.is_some() {
Color::Error.color(cx)
} else {
cx.theme().colors().border
};
let replacement_border = cx.theme().colors().border;
let container_width = window.viewport_size().width;
let input_width = SearchInputWidth::calc_width(container_width);
let input_base_styles = || {
let input_base_styles = |border_color| {
h_flex()
.min_w_32()
.w(input_width)
@@ -246,7 +247,7 @@ impl Render for BufferSearchBar {
.pr_1()
.py_1()
.border_1()
.border_color(editor_border)
.border_color(border_color)
.rounded_lg()
};
@@ -256,7 +257,7 @@ impl Render for BufferSearchBar {
el.child(Label::new("Find in results").color(Color::Hint))
})
.child(
input_base_styles()
input_base_styles(query_border)
.id("editor-scroll")
.track_scroll(&self.editor_scroll_handle)
.child(self.render_text_input(&self.query_editor, color_override, cx))
@@ -430,11 +431,13 @@ impl Render for BufferSearchBar {
let replace_line = should_show_replace_input.then(|| {
h_flex()
.gap_2()
.child(input_base_styles().child(self.render_text_input(
&self.replacement_editor,
None,
cx,
)))
.child(
input_base_styles(replacement_border).child(self.render_text_input(
&self.replacement_editor,
None,
cx,
)),
)
.child(
h_flex()
.min_w_64()
@@ -775,6 +778,7 @@ impl BufferSearchBar {
pub fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context<Self>) {
self.dismissed = true;
self.query_error = None;
for searchable_item in self.searchable_items_with_matches.keys() {
if let Some(searchable_item) =
WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)

View File

@@ -195,6 +195,7 @@ pub struct ProjectSearch {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum InputPanel {
Query,
Replacement,
Exclude,
Include,
}
@@ -1962,7 +1963,7 @@ impl Render for ProjectSearchBar {
MultipleInputs,
}
let input_base_styles = |base_style: BaseStyle| {
let input_base_styles = |base_style: BaseStyle, panel: InputPanel| {
h_flex()
.min_w_32()
.map(|div| match base_style {
@@ -1974,11 +1975,11 @@ impl Render for ProjectSearchBar {
.pr_1()
.py_1()
.border_1()
.border_color(search.border_color_for(InputPanel::Query, cx))
.border_color(search.border_color_for(panel, cx))
.rounded_lg()
};
let query_column = input_base_styles(BaseStyle::SingleInput)
let query_column = input_base_styles(BaseStyle::SingleInput, InputPanel::Query)
.on_action(cx.listener(|this, action, window, cx| this.confirm(action, window, cx)))
.on_action(cx.listener(|this, action, window, cx| {
this.previous_history_query(action, window, cx)
@@ -2167,7 +2168,7 @@ impl Render for ProjectSearchBar {
.child(h_flex().min_w_64().child(mode_column).child(matches_column));
let replace_line = search.replace_enabled.then(|| {
let replace_column = input_base_styles(BaseStyle::SingleInput)
let replace_column = input_base_styles(BaseStyle::SingleInput, InputPanel::Replacement)
.child(self.render_text_input(&search.replacement_editor, cx));
let focus_handle = search.replacement_editor.read(cx).focus_handle(cx);
@@ -2241,7 +2242,7 @@ impl Render for ProjectSearchBar {
.gap_2()
.w(input_width)
.child(
input_base_styles(BaseStyle::MultipleInputs)
input_base_styles(BaseStyle::MultipleInputs, InputPanel::Include)
.on_action(cx.listener(|this, action, window, cx| {
this.previous_history_query(action, window, cx)
}))
@@ -2251,7 +2252,7 @@ impl Render for ProjectSearchBar {
.child(self.render_text_input(&search.included_files_editor, cx)),
)
.child(
input_base_styles(BaseStyle::MultipleInputs)
input_base_styles(BaseStyle::MultipleInputs, InputPanel::Exclude)
.on_action(cx.listener(|this, action, window, cx| {
this.previous_history_query(action, window, cx)
}))

View File

@@ -41,16 +41,14 @@ pub trait Summary: Clone {
fn add_summary(&mut self, summary: &Self, cx: &Self::Context);
}
/// This type exists because we can't implement Summary for () without causing
/// type resolution errors
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct Unit;
impl Summary for Unit {
/// Catch-all implementation for when you need something that implements [`Summary`] without a specific type.
/// We implement it on a &'static, as that avoids blanket impl collisions with `impl<T: Summary> Dimension for T`
/// (as we also need unit type to be a fill-in dimension)
impl Summary for &'static () {
type Context = ();
fn zero(_: &()) -> Self {
Unit
&()
}
fn add_summary(&mut self, _: &Self, _: &()) {}

View File

@@ -1,8 +1,8 @@
use editor::{DisplayPoint, Editor, movement};
use editor::{DisplayPoint, Editor, SelectionEffects, ToOffset, ToPoint, movement};
use gpui::{Action, actions};
use gpui::{Context, Window};
use language::{CharClassifier, CharKind};
use text::SelectionGoal;
use text::{Bias, SelectionGoal};
use crate::{
Vim,
@@ -341,6 +341,80 @@ impl Vim {
});
});
}
pub fn helix_replace(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
self.update_editor(window, cx, |_, editor, window, cx| {
editor.transact(window, cx, |editor, window, cx| {
let (map, selections) = editor.selections.all_display(cx);
// Store selection info for positioning after edit
let selection_info: Vec<_> = selections
.iter()
.map(|selection| {
let range = selection.range();
let start_offset = range.start.to_offset(&map, Bias::Left);
let end_offset = range.end.to_offset(&map, Bias::Left);
let was_empty = range.is_empty();
let was_reversed = selection.reversed;
(
map.buffer_snapshot.anchor_at(start_offset, Bias::Left),
end_offset - start_offset,
was_empty,
was_reversed,
)
})
.collect();
let mut edits = Vec::new();
for selection in &selections {
let mut range = selection.range();
// For empty selections, extend to replace one character
if range.is_empty() {
range.end = movement::saturating_right(&map, range.start);
}
let byte_range = range.start.to_offset(&map, Bias::Left)
..range.end.to_offset(&map, Bias::Left);
if !byte_range.is_empty() {
let replacement_text = text.repeat(byte_range.len());
edits.push((byte_range, replacement_text));
}
}
editor.edit(edits, cx);
// Restore selections based on original info
let snapshot = editor.buffer().read(cx).snapshot(cx);
let ranges: Vec<_> = selection_info
.into_iter()
.map(|(start_anchor, original_len, was_empty, was_reversed)| {
let start_point = start_anchor.to_point(&snapshot);
if was_empty {
// For cursor-only, collapse to start
start_point..start_point
} else {
// For selections, span the replaced text
let replacement_len = text.len() * original_len;
let end_offset = start_anchor.to_offset(&snapshot) + replacement_len;
let end_point = snapshot.offset_to_point(end_offset);
if was_reversed {
end_point..start_point
} else {
start_point..end_point
}
}
})
.collect();
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges(ranges);
});
});
});
self.switch_mode(Mode::HelixNormal, true, window, cx);
}
}
#[cfg(test)]
@@ -603,4 +677,30 @@ mod test {
Mode::Insert,
);
}
#[gpui::test]
async fn test_replace(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
// No selection (single character)
cx.set_state("ˇaa", Mode::HelixNormal);
cx.simulate_keystrokes("r x");
cx.assert_state("ˇxa", Mode::HelixNormal);
// Cursor at the beginning
cx.set_state("«ˇaa»", Mode::HelixNormal);
cx.simulate_keystrokes("r x");
cx.assert_state("«ˇxx»", Mode::HelixNormal);
// Cursor at the end
cx.set_state("«aaˇ»", Mode::HelixNormal);
cx.simulate_keystrokes("r x");
cx.assert_state("«xxˇ»", Mode::HelixNormal);
}
}

View File

@@ -1639,6 +1639,7 @@ impl Vim {
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
self.visual_replace(text, window, cx)
}
Mode::HelixNormal => self.helix_replace(&text, window, cx),
_ => self.clear_operator(window, cx),
},
Some(Operator::Digraph { first_char }) => {

View File

@@ -934,6 +934,10 @@ impl Render for PanelButtons {
h_flex()
.gap_1()
.when(
has_buttons && dock.position == DockPosition::Bottom,
|this| this.child(Divider::vertical().color(DividerColor::Border)),
)
.children(buttons)
.when(has_buttons && dock.position == DockPosition::Left, |this| {
this.child(Divider::vertical().color(DividerColor::Border))

View File

@@ -62,7 +62,7 @@ use std::{
},
time::{Duration, Instant},
};
use sum_tree::{Bias, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet, Unit};
use sum_tree::{Bias, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet};
use text::{LineEnding, Rope};
use util::{
ResultExt,
@@ -407,12 +407,12 @@ struct LocalRepositoryEntry {
}
impl sum_tree::Item for LocalRepositoryEntry {
type Summary = PathSummary<Unit>;
type Summary = PathSummary<&'static ()>;
fn summary(&self, _: &<Self::Summary as Summary>::Context) -> Self::Summary {
PathSummary {
max_path: self.work_directory.path_key().0,
item_summary: Unit,
item_summary: &(),
}
}
}
@@ -425,12 +425,6 @@ impl KeyedItem for LocalRepositoryEntry {
}
}
//impl LocalRepositoryEntry {
// pub fn repo(&self) -> &Arc<dyn GitRepository> {
// &self.repo_ptr
// }
//}
impl Deref for LocalRepositoryEntry {
type Target = WorkDirectory;
@@ -5417,7 +5411,7 @@ impl<'a> SeekTarget<'a, EntrySummary, TraversalProgress<'a>> for TraversalTarget
}
}
impl<'a> SeekTarget<'a, PathSummary<Unit>, TraversalProgress<'a>> for TraversalTarget<'_> {
impl<'a> SeekTarget<'a, PathSummary<&'static ()>, TraversalProgress<'a>> for TraversalTarget<'_> {
fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: &()) -> Ordering {
self.cmp_progress(cursor_location)
}