Compare commits
12 commits
main
...
v0.197.1-p
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f6f7762f32 | ||
![]() |
c015ef64dc | ||
![]() |
d3b2f604a9 | ||
![]() |
b8849d83e6 | ||
![]() |
77dda2eca8 | ||
![]() |
ece9dd2c43 | ||
![]() |
c60f37a044 | ||
![]() |
ca646e2951 | ||
![]() |
b5433a9a54 | ||
![]() |
4727ae35d2 | ||
![]() |
d61db1fae7 | ||
![]() |
45d211a555 |
20 changed files with 549 additions and 416 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -20170,7 +20170,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.197.0"
|
||||
version = "0.197.1"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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()
|
||||
.when(is_expanded, |this| this.mb_2())
|
||||
.child(
|
||||
|
@ -230,8 +237,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 {
|
||||
|
|
|
@ -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,
|
||||
|
@ -1901,85 +1905,96 @@ impl AgentPanel {
|
|||
)
|
||||
.anchor(Corner::TopRight)
|
||||
.with_handle(self.new_thread_menu_handle.clone())
|
||||
.menu(move |window, cx| {
|
||||
let active_thread = active_thread.clone();
|
||||
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
||||
menu = menu
|
||||
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
|
||||
this.header("Zed Agent")
|
||||
})
|
||||
.item(
|
||||
ContextMenuEntry::new("New Thread")
|
||||
.icon(IconName::NewThread)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(NewThread::default().boxed_clone(), cx);
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("New Text Thread")
|
||||
.icon(IconName::NewTextThread)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(NewTextThread.boxed_clone(), cx);
|
||||
}),
|
||||
)
|
||||
.when_some(active_thread, |this, active_thread| {
|
||||
let thread = active_thread.read(cx);
|
||||
.menu({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
let active_thread = active_thread.clone();
|
||||
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
||||
menu = menu
|
||||
.context(focus_handle.clone())
|
||||
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
|
||||
this.header("Zed Agent")
|
||||
})
|
||||
.item(
|
||||
ContextMenuEntry::new("New Thread")
|
||||
.icon(IconName::NewThread)
|
||||
.icon_color(Color::Muted)
|
||||
.action(NewThread::default().boxed_clone())
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(
|
||||
NewThread::default().boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("New Text Thread")
|
||||
.icon(IconName::NewTextThread)
|
||||
.icon_color(Color::Muted)
|
||||
.action(NewTextThread.boxed_clone())
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(NewTextThread.boxed_clone(), cx);
|
||||
}),
|
||||
)
|
||||
.when_some(active_thread, |this, active_thread| {
|
||||
let thread = active_thread.read(cx);
|
||||
|
||||
if !thread.is_empty() {
|
||||
let thread_id = thread.id().clone();
|
||||
this.item(
|
||||
ContextMenuEntry::new("New From Summary")
|
||||
.icon(IconName::NewFromSummary)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(NewThread {
|
||||
from_thread_id: Some(thread_id.clone()),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
|
||||
this.separator()
|
||||
.header("External Agents")
|
||||
.item(
|
||||
ContextMenuEntry::new("New Gemini Thread")
|
||||
.icon(IconName::AiGemini)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(
|
||||
NewExternalAgentThread {
|
||||
agent: Some(crate::ExternalAgent::Gemini),
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("New Claude Code Thread")
|
||||
.icon(IconName::AiClaude)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(
|
||||
NewExternalAgentThread {
|
||||
agent: Some(crate::ExternalAgent::ClaudeCode),
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
});
|
||||
menu
|
||||
}))
|
||||
if !thread.is_empty() {
|
||||
let thread_id = thread.id().clone();
|
||||
this.item(
|
||||
ContextMenuEntry::new("New From Summary")
|
||||
.icon(IconName::NewFromSummary)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(NewThread {
|
||||
from_thread_id: Some(thread_id.clone()),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
|
||||
this.separator()
|
||||
.header("External Agents")
|
||||
.item(
|
||||
ContextMenuEntry::new("New Gemini Thread")
|
||||
.icon(IconName::AiGemini)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(
|
||||
NewExternalAgentThread {
|
||||
agent: Some(crate::ExternalAgent::Gemini),
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("New Claude Code Thread")
|
||||
.icon(IconName::AiClaude)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(
|
||||
NewExternalAgentThread {
|
||||
agent: Some(
|
||||
crate::ExternalAgent::ClaudeCode,
|
||||
),
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
});
|
||||
menu
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
let agent_panel_menu = PopoverMenu::new("agent-options-menu")
|
||||
|
@ -2272,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,
|
||||
|
@ -2306,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)
|
||||
|
|
|
@ -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,
|
||||
|
@ -625,7 +630,7 @@ impl MessageEditor {
|
|||
.unwrap_or(false);
|
||||
|
||||
IconButton::new("follow-agent", IconName::Crosshair)
|
||||
.disabled(is_model_selected)
|
||||
.disabled(!is_model_selected)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.toggle_state(following)
|
||||
|
@ -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,
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -237,7 +247,7 @@ impl ZedAiOnboarding {
|
|||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(move |_, _window, cx| {
|
||||
telemetry::event!("Review Terms of Service Click");
|
||||
telemetry::event!("Review Terms of Service Clicked");
|
||||
cx.open_url(&zed_urls::terms_of_service(cx))
|
||||
}),
|
||||
)
|
||||
|
@ -248,7 +258,7 @@ impl ZedAiOnboarding {
|
|||
.on_click({
|
||||
let callback = self.accept_terms_of_service.clone();
|
||||
move |_, window, cx| {
|
||||
telemetry::event!("Accepted Terms of Service");
|
||||
telemetry::event!("Terms of Service Accepted");
|
||||
(callback)(window, cx)}
|
||||
}),
|
||||
)
|
||||
|
@ -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")
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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'");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -16837,7 +16837,7 @@ async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
|
||||
async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let cols = 4;
|
||||
|
|
|
@ -12,6 +12,7 @@ use language::{self, Buffer, Point};
|
|||
use project::Project;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cmp,
|
||||
ops::Range,
|
||||
pin::pin,
|
||||
sync::Arc,
|
||||
|
@ -45,38 +46,60 @@ impl TextDiffView {
|
|||
) -> Option<Task<Result<Entity<Self>>>> {
|
||||
let source_editor = diff_data.editor.clone();
|
||||
|
||||
let source_editor_buffer_and_range = source_editor.update(cx, |editor, cx| {
|
||||
let selection_data = source_editor.update(cx, |editor, cx| {
|
||||
let multibuffer = editor.buffer().read(cx);
|
||||
let source_buffer = multibuffer.as_singleton()?.clone();
|
||||
let selections = editor.selections.all::<Point>(cx);
|
||||
let buffer_snapshot = source_buffer.read(cx);
|
||||
let first_selection = selections.first()?;
|
||||
let selection_range = if first_selection.is_empty() {
|
||||
Point::new(0, 0)..buffer_snapshot.max_point()
|
||||
} else {
|
||||
first_selection.start..first_selection.end
|
||||
};
|
||||
let max_point = buffer_snapshot.max_point();
|
||||
|
||||
Some((source_buffer, selection_range))
|
||||
if first_selection.is_empty() {
|
||||
let full_range = Point::new(0, 0)..max_point;
|
||||
return Some((source_buffer, full_range));
|
||||
}
|
||||
|
||||
let start = first_selection.start;
|
||||
let end = first_selection.end;
|
||||
let expanded_start = Point::new(start.row, 0);
|
||||
|
||||
let expanded_end = if end.column > 0 {
|
||||
let next_row = end.row + 1;
|
||||
cmp::min(max_point, Point::new(next_row, 0))
|
||||
} else {
|
||||
end
|
||||
};
|
||||
Some((source_buffer, expanded_start..expanded_end))
|
||||
});
|
||||
|
||||
let Some((source_buffer, selected_range)) = source_editor_buffer_and_range else {
|
||||
let Some((source_buffer, expanded_selection_range)) = selection_data else {
|
||||
log::warn!("There should always be at least one selection in Zed. This is a bug.");
|
||||
return None;
|
||||
};
|
||||
|
||||
let clipboard_text = diff_data.clipboard_text.clone();
|
||||
|
||||
let workspace = workspace.weak_handle();
|
||||
|
||||
let diff_buffer = cx.new(|cx| {
|
||||
let source_buffer_snapshot = source_buffer.read(cx).snapshot();
|
||||
let diff = BufferDiff::new(&source_buffer_snapshot.text, cx);
|
||||
diff
|
||||
source_editor.update(cx, |source_editor, cx| {
|
||||
source_editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select_ranges(vec![
|
||||
expanded_selection_range.start..expanded_selection_range.end,
|
||||
]);
|
||||
})
|
||||
});
|
||||
|
||||
let clipboard_buffer =
|
||||
build_clipboard_buffer(clipboard_text, &source_buffer, selected_range.clone(), cx);
|
||||
let source_buffer_snapshot = source_buffer.read(cx).snapshot();
|
||||
let mut clipboard_text = diff_data.clipboard_text.clone();
|
||||
|
||||
if !clipboard_text.ends_with("\n") {
|
||||
clipboard_text.push_str("\n");
|
||||
}
|
||||
|
||||
let workspace = workspace.weak_handle();
|
||||
let diff_buffer = cx.new(|cx| BufferDiff::new(&source_buffer_snapshot.text, cx));
|
||||
let clipboard_buffer = build_clipboard_buffer(
|
||||
clipboard_text,
|
||||
&source_buffer,
|
||||
expanded_selection_range.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
let task = window.spawn(cx, async move |cx| {
|
||||
let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
|
||||
|
@ -89,7 +112,7 @@ impl TextDiffView {
|
|||
clipboard_buffer,
|
||||
source_editor,
|
||||
source_buffer,
|
||||
selected_range,
|
||||
expanded_selection_range,
|
||||
diff_buffer,
|
||||
project,
|
||||
window,
|
||||
|
@ -208,9 +231,9 @@ impl TextDiffView {
|
|||
}
|
||||
|
||||
fn build_clipboard_buffer(
|
||||
clipboard_text: String,
|
||||
text: String,
|
||||
source_buffer: &Entity<Buffer>,
|
||||
selected_range: Range<Point>,
|
||||
replacement_range: Range<Point>,
|
||||
cx: &mut App,
|
||||
) -> Entity<Buffer> {
|
||||
let source_buffer_snapshot = source_buffer.read(cx).snapshot();
|
||||
|
@ -219,9 +242,9 @@ fn build_clipboard_buffer(
|
|||
let language = source_buffer.read(cx).language().cloned();
|
||||
buffer.set_language(language, cx);
|
||||
|
||||
let range_start = source_buffer_snapshot.point_to_offset(selected_range.start);
|
||||
let range_end = source_buffer_snapshot.point_to_offset(selected_range.end);
|
||||
buffer.edit([(range_start..range_end, clipboard_text)], None, cx);
|
||||
let range_start = source_buffer_snapshot.point_to_offset(replacement_range.start);
|
||||
let range_end = source_buffer_snapshot.point_to_offset(replacement_range.end);
|
||||
buffer.edit([(range_start..range_end, text)], None, cx);
|
||||
|
||||
buffer
|
||||
})
|
||||
|
@ -293,7 +316,7 @@ impl Item for TextDiffView {
|
|||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
Some("Diff View Opened")
|
||||
Some("Selection Diff View Opened")
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
|
@ -395,21 +418,13 @@ pub fn selection_location_text(editor: &Editor, cx: &App) -> Option<String> {
|
|||
let buffer_snapshot = buffer.snapshot(cx);
|
||||
let first_selection = editor.selections.disjoint.first()?;
|
||||
|
||||
let (start_row, start_column, end_row, end_column) =
|
||||
if first_selection.start == first_selection.end {
|
||||
let max_point = buffer_snapshot.max_point();
|
||||
(0, 0, max_point.row, max_point.column)
|
||||
} else {
|
||||
let selection_start = first_selection.start.to_point(&buffer_snapshot);
|
||||
let selection_end = first_selection.end.to_point(&buffer_snapshot);
|
||||
let selection_start = first_selection.start.to_point(&buffer_snapshot);
|
||||
let selection_end = first_selection.end.to_point(&buffer_snapshot);
|
||||
|
||||
(
|
||||
selection_start.row,
|
||||
selection_start.column,
|
||||
selection_end.row,
|
||||
selection_end.column,
|
||||
)
|
||||
};
|
||||
let start_row = selection_start.row;
|
||||
let start_column = selection_start.column;
|
||||
let end_row = selection_end.row;
|
||||
let end_column = selection_end.column;
|
||||
|
||||
let range_text = if start_row == end_row {
|
||||
format!("L{}:{}-{}", start_row + 1, start_column + 1, end_column + 1)
|
||||
|
@ -435,14 +450,13 @@ impl Render for TextDiffView {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use editor::{actions, test::editor_test_context::assert_state_with_diff};
|
||||
use editor::test::editor_test_context::assert_state_with_diff;
|
||||
use gpui::{TestAppContext, VisualContext};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use unindent::unindent;
|
||||
use util::path;
|
||||
use util::{path, test::marked_text_ranges};
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
|
@ -457,52 +471,236 @@ mod tests {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_diffing_clipboard_against_specific_selection(cx: &mut TestAppContext) {
|
||||
base_test(true, cx).await;
|
||||
async fn test_diffing_clipboard_against_empty_selection_uses_full_buffer_selection(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
base_test(
|
||||
path!("/test"),
|
||||
path!("/test/text.txt"),
|
||||
"def process_incoming_inventory(items, warehouse_id):\n pass\n",
|
||||
"def process_outgoing_inventory(items, warehouse_id):\n passˇ\n",
|
||||
&unindent(
|
||||
"
|
||||
- def process_incoming_inventory(items, warehouse_id):
|
||||
+ ˇdef process_outgoing_inventory(items, warehouse_id):
|
||||
pass
|
||||
",
|
||||
),
|
||||
"Clipboard ↔ text.txt @ L1:1-L3:1",
|
||||
&format!("Clipboard ↔ {} @ L1:1-L3:1", path!("test/text.txt")),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_diffing_clipboard_against_empty_selection_uses_full_buffer(
|
||||
async fn test_diffing_clipboard_against_multiline_selection_expands_to_full_lines(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
base_test(false, cx).await;
|
||||
base_test(
|
||||
path!("/test"),
|
||||
path!("/test/text.txt"),
|
||||
"def process_incoming_inventory(items, warehouse_id):\n pass\n",
|
||||
"«def process_outgoing_inventory(items, warehouse_id):\n passˇ»\n",
|
||||
&unindent(
|
||||
"
|
||||
- def process_incoming_inventory(items, warehouse_id):
|
||||
+ ˇdef process_outgoing_inventory(items, warehouse_id):
|
||||
pass
|
||||
",
|
||||
),
|
||||
"Clipboard ↔ text.txt @ L1:1-L3:1",
|
||||
&format!("Clipboard ↔ {} @ L1:1-L3:1", path!("test/text.txt")),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn base_test(select_all_text: bool, cx: &mut TestAppContext) {
|
||||
#[gpui::test]
|
||||
async fn test_diffing_clipboard_against_single_line_selection(cx: &mut TestAppContext) {
|
||||
base_test(
|
||||
path!("/test"),
|
||||
path!("/test/text.txt"),
|
||||
"a",
|
||||
"«bbˇ»",
|
||||
&unindent(
|
||||
"
|
||||
- a
|
||||
+ ˇbb",
|
||||
),
|
||||
"Clipboard ↔ text.txt @ L1:1-3",
|
||||
&format!("Clipboard ↔ {} @ L1:1-3", path!("test/text.txt")),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_diffing_clipboard_with_leading_whitespace_against_line(cx: &mut TestAppContext) {
|
||||
base_test(
|
||||
path!("/test"),
|
||||
path!("/test/text.txt"),
|
||||
" a",
|
||||
"«bbˇ»",
|
||||
&unindent(
|
||||
"
|
||||
- a
|
||||
+ ˇbb",
|
||||
),
|
||||
"Clipboard ↔ text.txt @ L1:1-3",
|
||||
&format!("Clipboard ↔ {} @ L1:1-3", path!("test/text.txt")),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_diffing_clipboard_against_line_with_leading_whitespace(cx: &mut TestAppContext) {
|
||||
base_test(
|
||||
path!("/test"),
|
||||
path!("/test/text.txt"),
|
||||
"a",
|
||||
" «bbˇ»",
|
||||
&unindent(
|
||||
"
|
||||
- a
|
||||
+ ˇ bb",
|
||||
),
|
||||
"Clipboard ↔ text.txt @ L1:1-7",
|
||||
&format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_diffing_clipboard_against_line_with_leading_whitespace_included_in_selection(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
base_test(
|
||||
path!("/test"),
|
||||
path!("/test/text.txt"),
|
||||
"a",
|
||||
"« bbˇ»",
|
||||
&unindent(
|
||||
"
|
||||
- a
|
||||
+ ˇ bb",
|
||||
),
|
||||
"Clipboard ↔ text.txt @ L1:1-7",
|
||||
&format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_diffing_clipboard_with_leading_whitespace_against_line_with_leading_whitespace(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
base_test(
|
||||
path!("/test"),
|
||||
path!("/test/text.txt"),
|
||||
" a",
|
||||
" «bbˇ»",
|
||||
&unindent(
|
||||
"
|
||||
- a
|
||||
+ ˇ bb",
|
||||
),
|
||||
"Clipboard ↔ text.txt @ L1:1-7",
|
||||
&format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_diffing_clipboard_with_leading_whitespace_against_line_with_leading_whitespace_included_in_selection(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
base_test(
|
||||
path!("/test"),
|
||||
path!("/test/text.txt"),
|
||||
" a",
|
||||
"« bbˇ»",
|
||||
&unindent(
|
||||
"
|
||||
- a
|
||||
+ ˇ bb",
|
||||
),
|
||||
"Clipboard ↔ text.txt @ L1:1-7",
|
||||
&format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_diffing_clipboard_against_partial_selection_expands_to_include_trailing_characters(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
base_test(
|
||||
path!("/test"),
|
||||
path!("/test/text.txt"),
|
||||
"a",
|
||||
"«bˇ»b",
|
||||
&unindent(
|
||||
"
|
||||
- a
|
||||
+ ˇbb",
|
||||
),
|
||||
"Clipboard ↔ text.txt @ L1:1-3",
|
||||
&format!("Clipboard ↔ {} @ L1:1-3", path!("test/text.txt")),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn base_test(
|
||||
project_root: &str,
|
||||
file_path: &str,
|
||||
clipboard_text: &str,
|
||||
editor_text: &str,
|
||||
expected_diff: &str,
|
||||
expected_tab_title: &str,
|
||||
expected_tab_tooltip: &str,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
|
||||
let file_name = std::path::Path::new(file_path)
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap();
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/test"),
|
||||
project_root,
|
||||
json!({
|
||||
"a": {
|
||||
"b": {
|
||||
"text.txt": "new line 1\nline 2\nnew line 3\nline 4"
|
||||
}
|
||||
}
|
||||
file_name: editor_text
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
|
||||
let project = Project::test(fs, [project_root.as_ref()], cx).await;
|
||||
|
||||
let (workspace, mut cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(path!("/test/a/b/text.txt"), cx)
|
||||
})
|
||||
.update(cx, |project, cx| project.open_local_buffer(file_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let editor = cx.new_window_entity(|window, cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, None, window, cx);
|
||||
editor.set_text("new line 1\nline 2\nnew line 3\nline 4\n", window, cx);
|
||||
|
||||
if select_all_text {
|
||||
editor.select_all(&actions::SelectAll, window, cx);
|
||||
}
|
||||
let (unmarked_text, selection_ranges) = marked_text_ranges(editor_text, false);
|
||||
editor.set_text(unmarked_text, window, cx);
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select_ranges(selection_ranges)
|
||||
});
|
||||
|
||||
editor
|
||||
});
|
||||
|
@ -511,7 +709,7 @@ mod tests {
|
|||
.update_in(cx, |workspace, window, cx| {
|
||||
TextDiffView::open(
|
||||
&DiffClipboardWithSelectionData {
|
||||
clipboard_text: "old line 1\nline 2\nold line 3\nline 4\n".to_string(),
|
||||
clipboard_text: clipboard_text.to_string(),
|
||||
editor,
|
||||
},
|
||||
workspace,
|
||||
|
@ -528,26 +726,14 @@ mod tests {
|
|||
assert_state_with_diff(
|
||||
&diff_view.read_with(cx, |diff_view, _| diff_view.diff_editor.clone()),
|
||||
&mut cx,
|
||||
&unindent(
|
||||
"
|
||||
- old line 1
|
||||
+ ˇnew line 1
|
||||
line 2
|
||||
- old line 3
|
||||
+ new line 3
|
||||
line 4
|
||||
",
|
||||
),
|
||||
expected_diff,
|
||||
);
|
||||
|
||||
diff_view.read_with(cx, |diff_view, cx| {
|
||||
assert_eq!(
|
||||
diff_view.tab_content_text(0, cx),
|
||||
"Clipboard ↔ text.txt @ L1:1-L5:1"
|
||||
);
|
||||
assert_eq!(diff_view.tab_content_text(0, cx), expected_tab_title);
|
||||
assert_eq!(
|
||||
diff_view.tab_tooltip_text(cx).unwrap(),
|
||||
format!("Clipboard ↔ {}", path!("test/a/b/text.txt @ L1:1-L5:1"))
|
||||
expected_tab_tooltip
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
|
|
|
@ -48,18 +48,29 @@ pub enum Model {
|
|||
#[serde(rename = "codestral-latest", alias = "codestral-latest")]
|
||||
#[default]
|
||||
CodestralLatest,
|
||||
|
||||
#[serde(rename = "mistral-large-latest", alias = "mistral-large-latest")]
|
||||
MistralLargeLatest,
|
||||
#[serde(rename = "mistral-medium-latest", alias = "mistral-medium-latest")]
|
||||
MistralMediumLatest,
|
||||
#[serde(rename = "mistral-small-latest", alias = "mistral-small-latest")]
|
||||
MistralSmallLatest,
|
||||
|
||||
#[serde(rename = "magistral-medium-latest", alias = "magistral-medium-latest")]
|
||||
MagistralMediumLatest,
|
||||
#[serde(rename = "magistral-small-latest", alias = "magistral-small-latest")]
|
||||
MagistralSmallLatest,
|
||||
|
||||
#[serde(rename = "open-mistral-nemo", alias = "open-mistral-nemo")]
|
||||
OpenMistralNemo,
|
||||
#[serde(rename = "open-codestral-mamba", alias = "open-codestral-mamba")]
|
||||
OpenCodestralMamba,
|
||||
|
||||
#[serde(rename = "devstral-medium-latest", alias = "devstral-medium-latest")]
|
||||
DevstralMediumLatest,
|
||||
#[serde(rename = "devstral-small-latest", alias = "devstral-small-latest")]
|
||||
DevstralSmallLatest,
|
||||
|
||||
#[serde(rename = "pixtral-12b-latest", alias = "pixtral-12b-latest")]
|
||||
Pixtral12BLatest,
|
||||
#[serde(rename = "pixtral-large-latest", alias = "pixtral-large-latest")]
|
||||
|
@ -89,8 +100,11 @@ impl Model {
|
|||
"mistral-large-latest" => Ok(Self::MistralLargeLatest),
|
||||
"mistral-medium-latest" => Ok(Self::MistralMediumLatest),
|
||||
"mistral-small-latest" => Ok(Self::MistralSmallLatest),
|
||||
"magistral-medium-latest" => Ok(Self::MagistralMediumLatest),
|
||||
"magistral-small-latest" => Ok(Self::MagistralSmallLatest),
|
||||
"open-mistral-nemo" => Ok(Self::OpenMistralNemo),
|
||||
"open-codestral-mamba" => Ok(Self::OpenCodestralMamba),
|
||||
"devstral-medium-latest" => Ok(Self::DevstralMediumLatest),
|
||||
"devstral-small-latest" => Ok(Self::DevstralSmallLatest),
|
||||
"pixtral-12b-latest" => Ok(Self::Pixtral12BLatest),
|
||||
"pixtral-large-latest" => Ok(Self::PixtralLargeLatest),
|
||||
|
@ -104,8 +118,11 @@ impl Model {
|
|||
Self::MistralLargeLatest => "mistral-large-latest",
|
||||
Self::MistralMediumLatest => "mistral-medium-latest",
|
||||
Self::MistralSmallLatest => "mistral-small-latest",
|
||||
Self::MagistralMediumLatest => "magistral-medium-latest",
|
||||
Self::MagistralSmallLatest => "magistral-small-latest",
|
||||
Self::OpenMistralNemo => "open-mistral-nemo",
|
||||
Self::OpenCodestralMamba => "open-codestral-mamba",
|
||||
Self::DevstralMediumLatest => "devstral-medium-latest",
|
||||
Self::DevstralSmallLatest => "devstral-small-latest",
|
||||
Self::Pixtral12BLatest => "pixtral-12b-latest",
|
||||
Self::PixtralLargeLatest => "pixtral-large-latest",
|
||||
|
@ -119,8 +136,11 @@ impl Model {
|
|||
Self::MistralLargeLatest => "mistral-large-latest",
|
||||
Self::MistralMediumLatest => "mistral-medium-latest",
|
||||
Self::MistralSmallLatest => "mistral-small-latest",
|
||||
Self::MagistralMediumLatest => "magistral-medium-latest",
|
||||
Self::MagistralSmallLatest => "magistral-small-latest",
|
||||
Self::OpenMistralNemo => "open-mistral-nemo",
|
||||
Self::OpenCodestralMamba => "open-codestral-mamba",
|
||||
Self::DevstralMediumLatest => "devstral-medium-latest",
|
||||
Self::DevstralSmallLatest => "devstral-small-latest",
|
||||
Self::Pixtral12BLatest => "pixtral-12b-latest",
|
||||
Self::PixtralLargeLatest => "pixtral-large-latest",
|
||||
|
@ -136,8 +156,11 @@ impl Model {
|
|||
Self::MistralLargeLatest => 131000,
|
||||
Self::MistralMediumLatest => 128000,
|
||||
Self::MistralSmallLatest => 32000,
|
||||
Self::MagistralMediumLatest => 40000,
|
||||
Self::MagistralSmallLatest => 40000,
|
||||
Self::OpenMistralNemo => 131000,
|
||||
Self::OpenCodestralMamba => 256000,
|
||||
Self::DevstralMediumLatest => 128000,
|
||||
Self::DevstralSmallLatest => 262144,
|
||||
Self::Pixtral12BLatest => 128000,
|
||||
Self::PixtralLargeLatest => 128000,
|
||||
|
@ -160,8 +183,11 @@ impl Model {
|
|||
| Self::MistralLargeLatest
|
||||
| Self::MistralMediumLatest
|
||||
| Self::MistralSmallLatest
|
||||
| Self::MagistralMediumLatest
|
||||
| Self::MagistralSmallLatest
|
||||
| Self::OpenMistralNemo
|
||||
| Self::OpenCodestralMamba
|
||||
| Self::DevstralMediumLatest
|
||||
| Self::DevstralSmallLatest
|
||||
| Self::Pixtral12BLatest
|
||||
| Self::PixtralLargeLatest => true,
|
||||
|
@ -177,8 +203,11 @@ impl Model {
|
|||
| Self::MistralSmallLatest => true,
|
||||
Self::CodestralLatest
|
||||
| Self::MistralLargeLatest
|
||||
| Self::MagistralMediumLatest
|
||||
| Self::MagistralSmallLatest
|
||||
| Self::OpenMistralNemo
|
||||
| Self::OpenCodestralMamba
|
||||
| Self::DevstralMediumLatest
|
||||
| Self::DevstralSmallLatest => false,
|
||||
Self::Custom {
|
||||
supports_images, ..
|
||||
|
|
|
@ -55,6 +55,7 @@ fn get_max_tokens(name: &str) -> u64 {
|
|||
"codellama" | "starcoder2" => 16384,
|
||||
"mistral" | "codestral" | "mixstral" | "llava" | "qwen2" | "qwen2.5-coder"
|
||||
| "dolphin-mixtral" => 32768,
|
||||
"magistral" => 40000,
|
||||
"llama3.1" | "llama3.2" | "llama3.3" | "phi3" | "phi3.5" | "phi4" | "command-r"
|
||||
| "qwen3" | "gemma3" | "deepseek-coder-v2" | "deepseek-v3" | "deepseek-r1" | "yi-coder"
|
||||
| "devstral" => 128000,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.197.0"
|
||||
version = "0.197.1"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
|
|
@ -1 +1 @@
|
|||
dev
|
||||
preview
|
Loading…
Add table
Add a link
Reference in a new issue