Compare commits
10 Commits
mcp2
...
agent-msg-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ac874ab31 | ||
|
|
2658b2801e | ||
|
|
2a9355a3d2 | ||
|
|
fa788a39a4 | ||
|
|
7cdd808db2 | ||
|
|
29332c1962 | ||
|
|
fab450e39d | ||
|
|
4fb540d6d2 | ||
|
|
1e2b0fcab6 | ||
|
|
0af690080b |
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -249,6 +249,7 @@ dependencies = [
|
||||
"prompt_store",
|
||||
"proto",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"rope",
|
||||
"rules_library",
|
||||
|
||||
@@ -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, .. }
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -123,6 +123,8 @@ actions!(
|
||||
ContinueWithBurnMode,
|
||||
/// Toggles burn mode for faster responses.
|
||||
ToggleBurnMode,
|
||||
EditAssistantMessage,
|
||||
EditUserMessage,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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'");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}))
|
||||
|
||||
@@ -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, _: &()) {}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user