Detailed changes
@@ -5111,7 +5111,6 @@ dependencies = [
"cloud_llm_client",
"collections",
"copilot",
- "credentials_provider",
"ctor",
"db",
"edit_prediction_context",
@@ -5275,7 +5274,6 @@ dependencies = [
"text",
"theme",
"ui",
- "ui_input",
"util",
"workspace",
"zed_actions",
@@ -8802,6 +8800,7 @@ dependencies = [
"cloud_api_types",
"cloud_llm_client",
"collections",
+ "credentials_provider",
"futures 0.3.31",
"gpui",
"http_client",
@@ -8820,6 +8819,7 @@ dependencies = [
"telemetry_events",
"thiserror 2.0.17",
"util",
+ "zed_env_vars",
]
[[package]]
@@ -8876,7 +8876,6 @@ dependencies = [
"util",
"vercel",
"x_ai",
- "zed_env_vars",
]
[[package]]
@@ -14778,6 +14777,8 @@ dependencies = [
"assets",
"bm25",
"client",
+ "copilot",
+ "edit_prediction",
"editor",
"feature_flags",
"fs",
@@ -14786,6 +14787,7 @@ dependencies = [
"gpui",
"heck 0.5.0",
"language",
+ "language_models",
"log",
"menu",
"node_runtime",
@@ -1410,8 +1410,9 @@
"proxy_no_verify": null,
},
"codestral": {
- "model": null,
- "max_tokens": null,
+ "api_url": "https://codestral.mistral.ai",
+ "model": "codestral-latest",
+ "max_tokens": 150,
},
// Whether edit predictions are enabled when editing text threads in the agent panel.
// This setting has no effect if globally disabled.
@@ -34,9 +34,9 @@ use project::{
};
use settings::{Settings, SettingsStore, update_settings_file};
use ui::{
- Button, ButtonStyle, Chip, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure,
- Divider, DividerColor, ElevationIndex, IconName, IconPosition, IconSize, Indicator, LabelSize,
- PopoverMenu, Switch, Tooltip, WithScrollbar, prelude::*,
+ ButtonStyle, Chip, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure, Divider,
+ DividerColor, ElevationIndex, Indicator, LabelSize, PopoverMenu, Switch, Tooltip,
+ WithScrollbar, prelude::*,
};
use util::ResultExt as _;
use workspace::{Workspace, create_and_open_local_file};
@@ -4,7 +4,7 @@ pub mod copilot_responses;
pub mod request;
mod sign_in;
-use crate::sign_in::initiate_sign_in_within_workspace;
+use crate::sign_in::initiate_sign_out;
use ::fs::Fs;
use anyhow::{Context as _, Result, anyhow};
use collections::{HashMap, HashSet};
@@ -28,12 +28,10 @@ use project::DisableAiSettings;
use request::StatusNotification;
use semver::Version;
use serde_json::json;
-use settings::Settings;
-use settings::SettingsStore;
-use sign_in::{reinstall_and_sign_in_within_workspace, sign_out_within_workspace};
-use std::collections::hash_map::Entry;
+use settings::{Settings, SettingsStore};
use std::{
any::TypeId,
+ collections::hash_map::Entry,
env,
ffi::OsString,
mem,
@@ -42,12 +40,14 @@ use std::{
sync::Arc,
};
use sum_tree::Dimensions;
-use util::rel_path::RelPath;
-use util::{ResultExt, fs::remove_matching};
+use util::{ResultExt, fs::remove_matching, rel_path::RelPath};
use workspace::Workspace;
pub use crate::copilot_edit_prediction_delegate::CopilotEditPredictionDelegate;
-pub use crate::sign_in::{CopilotCodeVerification, initiate_sign_in, reinstall_and_sign_in};
+pub use crate::sign_in::{
+ ConfigurationMode, ConfigurationView, CopilotCodeVerification, initiate_sign_in,
+ reinstall_and_sign_in,
+};
actions!(
copilot,
@@ -98,21 +98,14 @@ pub fn init(
.detach();
cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
- workspace.register_action(|workspace, _: &SignIn, window, cx| {
- if let Some(copilot) = Copilot::global(cx) {
- let is_reinstall = false;
- initiate_sign_in_within_workspace(workspace, copilot, is_reinstall, window, cx);
- }
+ workspace.register_action(|_, _: &SignIn, window, cx| {
+ initiate_sign_in(window, cx);
});
- workspace.register_action(|workspace, _: &Reinstall, window, cx| {
- if let Some(copilot) = Copilot::global(cx) {
- reinstall_and_sign_in_within_workspace(workspace, copilot, window, cx);
- }
+ workspace.register_action(|_, _: &Reinstall, window, cx| {
+ reinstall_and_sign_in(window, cx);
});
- workspace.register_action(|workspace, _: &SignOut, _window, cx| {
- if let Some(copilot) = Copilot::global(cx) {
- sign_out_within_workspace(workspace, copilot, cx);
- }
+ workspace.register_action(|_, _: &SignOut, window, cx| {
+ initiate_sign_out(window, cx);
});
})
.detach();
@@ -375,7 +368,7 @@ impl Copilot {
}
}
- fn start_copilot(
+ pub fn start_copilot(
&mut self,
check_edit_prediction_provider: bool,
awaiting_sign_in_after_start: bool,
@@ -563,6 +556,14 @@ impl Copilot {
let server = start_language_server.await;
this.update(cx, |this, cx| {
cx.notify();
+
+ if env::var("ZED_FORCE_COPILOT_ERROR").is_ok() {
+ this.server = CopilotServer::Error(
+ "Forced error for testing (ZED_FORCE_COPILOT_ERROR)".into(),
+ );
+ return;
+ }
+
match server {
Ok((server, status)) => {
this.server = CopilotServer::Running(RunningCopilotServer {
@@ -584,7 +585,17 @@ impl Copilot {
.ok();
}
- pub(crate) fn sign_in(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
+ pub fn is_authenticated(&self) -> bool {
+ return matches!(
+ self.server,
+ CopilotServer::Running(RunningCopilotServer {
+ sign_in_status: SignInStatus::Authorized,
+ ..
+ })
+ );
+ }
+
+ pub fn sign_in(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
if let CopilotServer::Running(server) = &mut self.server {
let task = match &server.sign_in_status {
SignInStatus::Authorized => Task::ready(Ok(())).shared(),
@@ -1,160 +1,151 @@
use crate::{Copilot, Status, request::PromptUserDeviceFlow};
+use anyhow::Context as _;
use gpui::{
- Animation, AnimationExt, App, ClipboardItem, Context, DismissEvent, Element, Entity,
- EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, MouseDownEvent,
- ParentElement, Render, Styled, Subscription, Transformation, Window, div, percentage, svg,
+ App, ClipboardItem, Context, DismissEvent, Element, Entity, EventEmitter, FocusHandle,
+ Focusable, InteractiveElement, IntoElement, MouseDownEvent, ParentElement, Render, Styled,
+ Subscription, Window, WindowBounds, WindowOptions, div, point,
};
-use std::time::Duration;
-use ui::{Button, Label, Vector, VectorName, prelude::*};
+use ui::{ButtonLike, CommonAnimationExt, ConfiguredApiCard, Vector, VectorName, prelude::*};
use util::ResultExt as _;
-use workspace::notifications::NotificationId;
-use workspace::{ModalView, Toast, Workspace};
+use workspace::{Toast, Workspace, notifications::NotificationId};
const COPILOT_SIGN_UP_URL: &str = "https://github.com/features/copilot";
+const ERROR_LABEL: &str =
+ "Copilot had issues starting. You can try reinstalling it and signing in again.";
struct CopilotStatusToast;
pub fn initiate_sign_in(window: &mut Window, cx: &mut App) {
+ let is_reinstall = false;
+ initiate_sign_in_impl(is_reinstall, window, cx)
+}
+
+pub fn initiate_sign_out(window: &mut Window, cx: &mut App) {
let Some(copilot) = Copilot::global(cx) else {
return;
};
- let Some(workspace) = window.root::<Workspace>().flatten() else {
- return;
- };
- workspace.update(cx, |workspace, cx| {
- let is_reinstall = false;
- initiate_sign_in_within_workspace(workspace, copilot, is_reinstall, window, cx)
- });
+
+ copilot_toast(Some("Signing out of Copilotโฆ"), window, cx);
+
+ let sign_out_task = copilot.update(cx, |copilot, cx| copilot.sign_out(cx));
+ window
+ .spawn(cx, async move |cx| match sign_out_task.await {
+ Ok(()) => {
+ cx.update(|window, cx| copilot_toast(Some("Signed out of Copilot"), window, cx))
+ }
+ Err(err) => cx.update(|window, cx| {
+ if let Some(workspace) = window.root::<Workspace>().flatten() {
+ workspace.update(cx, |workspace, cx| {
+ workspace.show_error(&err, cx);
+ })
+ } else {
+ log::error!("{:?}", err);
+ }
+ }),
+ })
+ .detach();
}
pub fn reinstall_and_sign_in(window: &mut Window, cx: &mut App) {
let Some(copilot) = Copilot::global(cx) else {
return;
};
+ let _ = copilot.update(cx, |copilot, cx| copilot.reinstall(cx));
+ let is_reinstall = true;
+ initiate_sign_in_impl(is_reinstall, window, cx);
+}
+
+fn open_copilot_code_verification_window(copilot: &Entity<Copilot>, window: &Window, cx: &mut App) {
+ let current_window_center = window.bounds().center();
+ let height = px(450.);
+ let width = px(350.);
+ let window_bounds = WindowBounds::Windowed(gpui::bounds(
+ current_window_center - point(height / 2.0, width / 2.0),
+ gpui::size(height, width),
+ ));
+ cx.open_window(
+ WindowOptions {
+ kind: gpui::WindowKind::PopUp,
+ window_bounds: Some(window_bounds),
+ is_resizable: false,
+ is_movable: true,
+ titlebar: Some(gpui::TitlebarOptions {
+ appears_transparent: true,
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ |window, cx| cx.new(|cx| CopilotCodeVerification::new(&copilot, window, cx)),
+ )
+ .context("Failed to open Copilot code verification window")
+ .log_err();
+}
+
+fn copilot_toast(message: Option<&'static str>, window: &Window, cx: &mut App) {
+ const NOTIFICATION_ID: NotificationId = NotificationId::unique::<CopilotStatusToast>();
+
let Some(workspace) = window.root::<Workspace>().flatten() else {
return;
};
- workspace.update(cx, |workspace, cx| {
- reinstall_and_sign_in_within_workspace(workspace, copilot, window, cx);
- });
-}
-pub fn reinstall_and_sign_in_within_workspace(
- workspace: &mut Workspace,
- copilot: Entity<Copilot>,
- window: &mut Window,
- cx: &mut Context<Workspace>,
-) {
- let _ = copilot.update(cx, |copilot, cx| copilot.reinstall(cx));
- let is_reinstall = true;
- initiate_sign_in_within_workspace(workspace, copilot, is_reinstall, window, cx);
+ workspace.update(cx, |workspace, cx| match message {
+ Some(message) => workspace.show_toast(Toast::new(NOTIFICATION_ID, message), cx),
+ None => workspace.dismiss_toast(&NOTIFICATION_ID, cx),
+ });
}
-pub fn initiate_sign_in_within_workspace(
- workspace: &mut Workspace,
- copilot: Entity<Copilot>,
- is_reinstall: bool,
- window: &mut Window,
- cx: &mut Context<Workspace>,
-) {
+pub fn initiate_sign_in_impl(is_reinstall: bool, window: &mut Window, cx: &mut App) {
+ let Some(copilot) = Copilot::global(cx) else {
+ return;
+ };
if matches!(copilot.read(cx).status(), Status::Disabled) {
copilot.update(cx, |copilot, cx| copilot.start_copilot(false, true, cx));
}
match copilot.read(cx).status() {
Status::Starting { task } => {
- workspace.show_toast(
- Toast::new(
- NotificationId::unique::<CopilotStatusToast>(),
- if is_reinstall {
- "Copilot is reinstalling..."
- } else {
- "Copilot is starting..."
- },
- ),
+ copilot_toast(
+ Some(if is_reinstall {
+ "Copilot is reinstallingโฆ"
+ } else {
+ "Copilot is startingโฆ"
+ }),
+ window,
cx,
);
- cx.spawn_in(window, async move |workspace, cx| {
- task.await;
- if let Some(copilot) = cx.update(|_window, cx| Copilot::global(cx)).ok().flatten() {
- workspace
- .update_in(cx, |workspace, window, cx| {
- match copilot.read(cx).status() {
- Status::Authorized => workspace.show_toast(
- Toast::new(
- NotificationId::unique::<CopilotStatusToast>(),
- "Copilot has started.",
- ),
- cx,
- ),
- _ => {
- workspace.dismiss_toast(
- &NotificationId::unique::<CopilotStatusToast>(),
- cx,
- );
- copilot
- .update(cx, |copilot, cx| copilot.sign_in(cx))
- .detach_and_log_err(cx);
- workspace.toggle_modal(window, cx, |_, cx| {
- CopilotCodeVerification::new(&copilot, cx)
- });
- }
+ window
+ .spawn(cx, async move |cx| {
+ task.await;
+ cx.update(|window, cx| {
+ let Some(copilot) = Copilot::global(cx) else {
+ return;
+ };
+ match copilot.read(cx).status() {
+ Status::Authorized => {
+ copilot_toast(Some("Copilot has started."), window, cx)
}
- })
- .log_err();
- }
- })
- .detach();
+ _ => {
+ copilot_toast(None, window, cx);
+ copilot
+ .update(cx, |copilot, cx| copilot.sign_in(cx))
+ .detach_and_log_err(cx);
+ open_copilot_code_verification_window(&copilot, window, cx);
+ }
+ }
+ })
+ .log_err();
+ })
+ .detach();
}
_ => {
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.detach();
- workspace.toggle_modal(window, cx, |_, cx| {
- CopilotCodeVerification::new(&copilot, cx)
- });
+ open_copilot_code_verification_window(&copilot, window, cx);
}
}
}
-pub fn sign_out_within_workspace(
- workspace: &mut Workspace,
- copilot: Entity<Copilot>,
- cx: &mut Context<Workspace>,
-) {
- workspace.show_toast(
- Toast::new(
- NotificationId::unique::<CopilotStatusToast>(),
- "Signing out of Copilot...",
- ),
- cx,
- );
- let sign_out_task = copilot.update(cx, |copilot, cx| copilot.sign_out(cx));
- cx.spawn(async move |workspace, cx| match sign_out_task.await {
- Ok(()) => {
- workspace
- .update(cx, |workspace, cx| {
- workspace.show_toast(
- Toast::new(
- NotificationId::unique::<CopilotStatusToast>(),
- "Signed out of Copilot.",
- ),
- cx,
- )
- })
- .ok();
- }
- Err(err) => {
- workspace
- .update(cx, |workspace, cx| {
- workspace.show_error(&err, cx);
- })
- .ok();
- }
- })
- .detach();
-}
-
pub struct CopilotCodeVerification {
status: Status,
connect_clicked: bool,
@@ -170,23 +161,27 @@ impl Focusable for CopilotCodeVerification {
}
impl EventEmitter<DismissEvent> for CopilotCodeVerification {}
-impl ModalView for CopilotCodeVerification {
- fn on_before_dismiss(
- &mut self,
- _: &mut Window,
- cx: &mut Context<Self>,
- ) -> workspace::DismissDecision {
- self.copilot.update(cx, |copilot, cx| {
- if matches!(copilot.status(), Status::SigningIn { .. }) {
- copilot.sign_out(cx).detach_and_log_err(cx);
+
+impl CopilotCodeVerification {
+ pub fn new(copilot: &Entity<Copilot>, window: &mut Window, cx: &mut Context<Self>) -> Self {
+ window.on_window_should_close(cx, |window, cx| {
+ if let Some(this) = window.root::<CopilotCodeVerification>().flatten() {
+ this.update(cx, |this, cx| {
+ this.before_dismiss(cx);
+ });
}
+ true
});
- workspace::DismissDecision::Dismiss(true)
- }
-}
+ cx.subscribe_in(
+ &cx.entity(),
+ window,
+ |this, _, _: &DismissEvent, window, cx| {
+ window.remove_window();
+ this.before_dismiss(cx);
+ },
+ )
+ .detach();
-impl CopilotCodeVerification {
- pub fn new(copilot: &Entity<Copilot>, cx: &mut Context<Self>) -> Self {
let status = copilot.read(cx).status();
Self {
status,
@@ -215,45 +210,45 @@ impl CopilotCodeVerification {
.read_from_clipboard()
.map(|item| item.text().as_ref() == Some(&data.user_code))
.unwrap_or(false);
- h_flex()
- .w_full()
- .p_1()
- .border_1()
- .border_muted(cx)
- .rounded_sm()
- .cursor_pointer()
- .justify_between()
- .on_mouse_down(gpui::MouseButton::Left, {
+
+ ButtonLike::new("copy-button")
+ .full_width()
+ .style(ButtonStyle::Tinted(ui::TintColor::Accent))
+ .size(ButtonSize::Medium)
+ .child(
+ h_flex()
+ .w_full()
+ .p_1()
+ .justify_between()
+ .child(Label::new(data.user_code.clone()))
+ .child(Label::new(if copied { "Copied!" } else { "Copy" })),
+ )
+ .on_click({
let user_code = data.user_code.clone();
move |_, window, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(user_code.clone()));
window.refresh();
}
})
- .child(div().flex_1().child(Label::new(data.user_code.clone())))
- .child(div().flex_none().px_1().child(Label::new(if copied {
- "Copied!"
- } else {
- "Copy"
- })))
}
fn render_prompting_modal(
connect_clicked: bool,
data: &PromptUserDeviceFlow,
-
cx: &mut Context<Self>,
) -> impl Element {
let connect_button_label = if connect_clicked {
- "Waiting for connection..."
+ "Waiting for connectionโฆ"
} else {
"Connect to GitHub"
};
+
v_flex()
.flex_1()
- .gap_2()
+ .gap_2p5()
.items_center()
- .child(Headline::new("Use GitHub Copilot in Zed.").size(HeadlineSize::Large))
+ .text_center()
+ .child(Headline::new("Use GitHub Copilot in Zed").size(HeadlineSize::Large))
.child(
Label::new("Using Copilot requires an active subscription on GitHub.")
.color(Color::Muted),
@@ -261,83 +256,119 @@ impl CopilotCodeVerification {
.child(Self::render_device_code(data, cx))
.child(
Label::new("Paste this code into GitHub after clicking the button below.")
- .size(ui::LabelSize::Small),
- )
- .child(
- Button::new("connect-button", connect_button_label)
- .on_click({
- let verification_uri = data.verification_uri.clone();
- cx.listener(move |this, _, _window, cx| {
- cx.open_url(&verification_uri);
- this.connect_clicked = true;
- })
- })
- .full_width()
- .style(ButtonStyle::Filled),
+ .color(Color::Muted),
)
.child(
- Button::new("copilot-enable-cancel-button", "Cancel")
- .full_width()
- .on_click(cx.listener(|_, _, _, cx| {
- cx.emit(DismissEvent);
- })),
+ v_flex()
+ .w_full()
+ .gap_1()
+ .child(
+ Button::new("connect-button", connect_button_label)
+ .full_width()
+ .style(ButtonStyle::Outlined)
+ .size(ButtonSize::Medium)
+ .on_click({
+ let verification_uri = data.verification_uri.clone();
+ cx.listener(move |this, _, _window, cx| {
+ cx.open_url(&verification_uri);
+ this.connect_clicked = true;
+ })
+ }),
+ )
+ .child(
+ Button::new("copilot-enable-cancel-button", "Cancel")
+ .full_width()
+ .size(ButtonSize::Medium)
+ .on_click(cx.listener(|_, _, _, cx| {
+ cx.emit(DismissEvent);
+ })),
+ ),
)
}
fn render_enabled_modal(cx: &mut Context<Self>) -> impl Element {
v_flex()
.gap_2()
+ .text_center()
+ .justify_center()
.child(Headline::new("Copilot Enabled!").size(HeadlineSize::Large))
- .child(Label::new(
- "You can update your settings or sign out from the Copilot menu in the status bar.",
- ))
+ .child(Label::new("You're all set to use GitHub Copilot.").color(Color::Muted))
.child(
Button::new("copilot-enabled-done-button", "Done")
.full_width()
+ .style(ButtonStyle::Outlined)
+ .size(ButtonSize::Medium)
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
)
}
fn render_unauthorized_modal(cx: &mut Context<Self>) -> impl Element {
- v_flex()
- .child(Headline::new("You must have an active GitHub Copilot subscription.").size(HeadlineSize::Large))
+ let description = "Enable Copilot by connecting your existing license once you have subscribed or renewed your subscription.";
- .child(Label::new(
- "You can enable Copilot by connecting your existing license once you have subscribed or renewed your subscription.",
- ).color(Color::Warning))
+ v_flex()
+ .gap_2()
+ .text_center()
+ .justify_center()
+ .child(
+ Headline::new("You must have an active GitHub Copilot subscription.")
+ .size(HeadlineSize::Large),
+ )
+ .child(Label::new(description).color(Color::Warning))
.child(
Button::new("copilot-subscribe-button", "Subscribe on GitHub")
.full_width()
+ .style(ButtonStyle::Outlined)
+ .size(ButtonSize::Medium)
.on_click(|_, _, cx| cx.open_url(COPILOT_SIGN_UP_URL)),
)
.child(
Button::new("copilot-subscribe-cancel-button", "Cancel")
.full_width()
+ .size(ButtonSize::Medium)
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
)
}
- fn render_loading(window: &mut Window, _: &mut Context<Self>) -> impl Element {
- let loading_icon = svg()
- .size_8()
- .path(IconName::ArrowCircle.path())
- .text_color(window.text_style().color)
- .with_animation(
- "icon_circle_arrow",
- Animation::new(Duration::from_secs(2)).repeat(),
- |svg, delta| svg.with_transformation(Transformation::rotate(percentage(delta))),
- );
+ fn render_error_modal(_cx: &mut Context<Self>) -> impl Element {
+ v_flex()
+ .gap_2()
+ .text_center()
+ .justify_center()
+ .child(Headline::new("An Error Happened").size(HeadlineSize::Large))
+ .child(Label::new(ERROR_LABEL).color(Color::Muted))
+ .child(
+ Button::new("copilot-subscribe-button", "Reinstall Copilot and Sign In")
+ .full_width()
+ .style(ButtonStyle::Outlined)
+ .size(ButtonSize::Medium)
+ .icon(IconName::Download)
+ .icon_color(Color::Muted)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::Small)
+ .on_click(|_, window, cx| reinstall_and_sign_in(window, cx)),
+ )
+ }
- h_flex().justify_center().child(loading_icon)
+ fn before_dismiss(
+ &mut self,
+ cx: &mut Context<'_, CopilotCodeVerification>,
+ ) -> workspace::DismissDecision {
+ self.copilot.update(cx, |copilot, cx| {
+ if matches!(copilot.status(), Status::SigningIn { .. }) {
+ copilot.sign_out(cx).detach_and_log_err(cx);
+ }
+ });
+ workspace::DismissDecision::Dismiss(true)
}
}
impl Render for CopilotCodeVerification {
- fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let prompt = match &self.status {
- Status::SigningIn { prompt: None } => {
- Self::render_loading(window, cx).into_any_element()
- }
+ Status::SigningIn { prompt: None } => Icon::new(IconName::ArrowCircle)
+ .color(Color::Muted)
+ .with_rotate_animation(2)
+ .into_any_element(),
Status::SigningIn {
prompt: Some(prompt),
} => Self::render_prompting_modal(self.connect_clicked, prompt, cx).into_any_element(),
@@ -349,17 +380,20 @@ impl Render for CopilotCodeVerification {
self.connect_clicked = false;
Self::render_enabled_modal(cx).into_any_element()
}
+ Status::Error(..) => Self::render_error_modal(cx).into_any_element(),
_ => div().into_any_element(),
};
v_flex()
- .id("copilot code verification")
+ .id("copilot_code_verification")
.track_focus(&self.focus_handle(cx))
- .elevation_3(cx)
- .w_96()
- .items_center()
- .p_4()
+ .size_full()
+ .px_4()
+ .py_8()
.gap_2()
+ .items_center()
+ .justify_center()
+ .elevation_3(cx)
.on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
cx.emit(DismissEvent);
}))
@@ -373,3 +407,243 @@ impl Render for CopilotCodeVerification {
.child(prompt)
}
}
+
+pub struct ConfigurationView {
+ copilot_status: Option<Status>,
+ is_authenticated: fn(cx: &App) -> bool,
+ edit_prediction: bool,
+ _subscription: Option<Subscription>,
+}
+
+pub enum ConfigurationMode {
+ Chat,
+ EditPrediction,
+}
+
+impl ConfigurationView {
+ pub fn new(
+ is_authenticated: fn(cx: &App) -> bool,
+ mode: ConfigurationMode,
+ cx: &mut Context<Self>,
+ ) -> Self {
+ let copilot = Copilot::global(cx);
+
+ Self {
+ copilot_status: copilot.as_ref().map(|copilot| copilot.read(cx).status()),
+ is_authenticated,
+ edit_prediction: matches!(mode, ConfigurationMode::EditPrediction),
+ _subscription: copilot.as_ref().map(|copilot| {
+ cx.observe(copilot, |this, model, cx| {
+ this.copilot_status = Some(model.read(cx).status());
+ cx.notify();
+ })
+ }),
+ }
+ }
+}
+
+impl ConfigurationView {
+ fn is_starting(&self) -> bool {
+ matches!(&self.copilot_status, Some(Status::Starting { .. }))
+ }
+
+ fn is_signing_in(&self) -> bool {
+ matches!(
+ &self.copilot_status,
+ Some(Status::SigningIn { .. })
+ | Some(Status::SignedOut {
+ awaiting_signing_in: true
+ })
+ )
+ }
+
+ fn is_error(&self) -> bool {
+ matches!(&self.copilot_status, Some(Status::Error(_)))
+ }
+
+ fn has_no_status(&self) -> bool {
+ self.copilot_status.is_none()
+ }
+
+ fn loading_message(&self) -> Option<SharedString> {
+ if self.is_starting() {
+ Some("Starting Copilotโฆ".into())
+ } else if self.is_signing_in() {
+ Some("Signing into Copilotโฆ".into())
+ } else {
+ None
+ }
+ }
+
+ fn render_loading_button(
+ &self,
+ label: impl Into<SharedString>,
+ edit_prediction: bool,
+ ) -> impl IntoElement {
+ ButtonLike::new("loading_button")
+ .disabled(true)
+ .style(ButtonStyle::Outlined)
+ .when(edit_prediction, |this| this.size(ButtonSize::Medium))
+ .child(
+ h_flex()
+ .w_full()
+ .gap_1()
+ .justify_center()
+ .child(
+ Icon::new(IconName::ArrowCircle)
+ .size(IconSize::Small)
+ .color(Color::Muted)
+ .with_rotate_animation(4),
+ )
+ .child(Label::new(label)),
+ )
+ }
+
+ fn render_sign_in_button(&self, edit_prediction: bool) -> impl IntoElement {
+ let label = if edit_prediction {
+ "Sign in to GitHub"
+ } else {
+ "Sign in to use GitHub Copilot"
+ };
+
+ Button::new("sign_in", label)
+ .map(|this| {
+ if edit_prediction {
+ this.size(ButtonSize::Medium)
+ } else {
+ this.full_width()
+ }
+ })
+ .style(ButtonStyle::Outlined)
+ .icon(IconName::Github)
+ .icon_color(Color::Muted)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::Small)
+ .on_click(|_, window, cx| initiate_sign_in(window, cx))
+ }
+
+ fn render_reinstall_button(&self, edit_prediction: bool) -> impl IntoElement {
+ let label = if edit_prediction {
+ "Reinstall and Sign in"
+ } else {
+ "Reinstall Copilot and Sign in"
+ };
+
+ Button::new("reinstall_and_sign_in", label)
+ .map(|this| {
+ if edit_prediction {
+ this.size(ButtonSize::Medium)
+ } else {
+ this.full_width()
+ }
+ })
+ .style(ButtonStyle::Outlined)
+ .icon(IconName::Download)
+ .icon_color(Color::Muted)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::Small)
+ .on_click(|_, window, cx| reinstall_and_sign_in(window, cx))
+ }
+
+ fn render_for_edit_prediction(&self) -> impl IntoElement {
+ let container = |description: SharedString, action: AnyElement| {
+ h_flex()
+ .pt_2p5()
+ .w_full()
+ .justify_between()
+ .child(
+ v_flex()
+ .w_full()
+ .max_w_1_2()
+ .child(Label::new("Authenticate To Use"))
+ .child(
+ Label::new(description)
+ .color(Color::Muted)
+ .size(LabelSize::Small),
+ ),
+ )
+ .child(action)
+ };
+
+ let start_label = "To use Copilot for edit predictions, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot subscription.".into();
+ let no_status_label = "Copilot requires an active GitHub Copilot subscription. Please ensure Copilot is configured and try again, or use a different edit predictions provider.".into();
+
+ if let Some(msg) = self.loading_message() {
+ container(
+ start_label,
+ self.render_loading_button(msg, true).into_any_element(),
+ )
+ .into_any_element()
+ } else if self.is_error() {
+ container(
+ ERROR_LABEL.into(),
+ self.render_reinstall_button(true).into_any_element(),
+ )
+ .into_any_element()
+ } else if self.has_no_status() {
+ container(
+ no_status_label,
+ self.render_sign_in_button(true).into_any_element(),
+ )
+ .into_any_element()
+ } else {
+ container(
+ start_label,
+ self.render_sign_in_button(true).into_any_element(),
+ )
+ .into_any_element()
+ }
+ }
+
+ fn render_for_chat(&self) -> impl IntoElement {
+ let start_label = "To use Zed's agent with GitHub Copilot, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot Chat subscription.";
+ let no_status_label = "Copilot Chat requires an active GitHub Copilot subscription. Please ensure Copilot is configured and try again, or use a different LLM provider.";
+
+ if let Some(msg) = self.loading_message() {
+ v_flex()
+ .gap_2()
+ .child(Label::new(start_label))
+ .child(self.render_loading_button(msg, false))
+ .into_any_element()
+ } else if self.is_error() {
+ v_flex()
+ .gap_2()
+ .child(Label::new(ERROR_LABEL))
+ .child(self.render_reinstall_button(false))
+ .into_any_element()
+ } else if self.has_no_status() {
+ v_flex()
+ .gap_2()
+ .child(Label::new(no_status_label))
+ .child(self.render_sign_in_button(false))
+ .into_any_element()
+ } else {
+ v_flex()
+ .gap_2()
+ .child(Label::new(start_label))
+ .child(self.render_sign_in_button(false))
+ .into_any_element()
+ }
+ }
+}
+
+impl Render for ConfigurationView {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let is_authenticated = self.is_authenticated;
+
+ if is_authenticated(cx) {
+ return ConfiguredApiCard::new("Authorized")
+ .button_label("Sign Out")
+ .on_click(|_, window, cx| {
+ initiate_sign_out(window, cx);
+ })
+ .into_any_element();
+ }
+
+ if self.edit_prediction {
+ self.render_for_edit_prediction().into_any_element()
+ } else {
+ self.render_for_chat().into_any_element()
+ }
+ }
+}
@@ -23,7 +23,6 @@ client.workspace = true
cloud_llm_client.workspace = true
collections.workspace = true
copilot.workspace = true
-credentials_provider.workspace = true
db.workspace = true
edit_prediction_types.workspace = true
edit_prediction_context.workspace = true
@@ -72,6 +72,7 @@ pub use crate::prediction::EditPrediction;
pub use crate::prediction::EditPredictionId;
use crate::prediction::EditPredictionResult;
pub use crate::sweep_ai::SweepAi;
+pub use language_model::ApiKeyState;
pub use telemetry_events::EditPredictionRating;
pub use zed_edit_prediction_delegate::ZedEditPredictionDelegate;
@@ -536,22 +537,12 @@ impl EditPredictionStore {
self.edit_prediction_model = model;
}
- pub fn has_sweep_api_token(&self) -> bool {
- self.sweep_ai
- .api_token
- .clone()
- .now_or_never()
- .flatten()
- .is_some()
+ pub fn has_sweep_api_token(&self, cx: &App) -> bool {
+ self.sweep_ai.api_token.read(cx).has_key()
}
- pub fn has_mercury_api_token(&self) -> bool {
- self.mercury
- .api_token
- .clone()
- .now_or_never()
- .flatten()
- .is_some()
+ pub fn has_mercury_api_token(&self, cx: &App) -> bool {
+ self.mercury.api_token.read(cx).has_key()
}
#[cfg(feature = "cli-support")]
@@ -1,40 +1,34 @@
+use crate::{
+ DebugEvent, EditPredictionFinishedDebugEvent, EditPredictionId, EditPredictionModelInput,
+ EditPredictionStartedDebugEvent, open_ai_response::text_from_response,
+ prediction::EditPredictionResult,
+};
use anyhow::{Context as _, Result};
-use credentials_provider::CredentialsProvider;
-use futures::{AsyncReadExt as _, FutureExt, future::Shared};
+use futures::AsyncReadExt as _;
use gpui::{
- App, AppContext as _, Task,
+ App, AppContext as _, Entity, SharedString, Task,
http_client::{self, AsyncBody, Method},
};
use language::{OffsetRangeExt as _, ToOffset, ToPoint as _};
+use language_model::{ApiKeyState, EnvVar, env_var};
use std::{mem, ops::Range, path::Path, sync::Arc, time::Instant};
use zeta_prompt::ZetaPromptInput;
-use crate::{
- DebugEvent, EditPredictionFinishedDebugEvent, EditPredictionId, EditPredictionModelInput,
- EditPredictionStartedDebugEvent, open_ai_response::text_from_response,
- prediction::EditPredictionResult,
-};
-
const MERCURY_API_URL: &str = "https://api.inceptionlabs.ai/v1/edit/completions";
const MAX_CONTEXT_TOKENS: usize = 150;
const MAX_REWRITE_TOKENS: usize = 350;
pub struct Mercury {
- pub api_token: Shared<Task<Option<String>>>,
+ pub api_token: Entity<ApiKeyState>,
}
impl Mercury {
- pub fn new(cx: &App) -> Self {
+ pub fn new(cx: &mut App) -> Self {
Mercury {
- api_token: load_api_token(cx).shared(),
+ api_token: mercury_api_token(cx),
}
}
- pub fn set_api_token(&mut self, api_token: Option<String>, cx: &mut App) -> Task<Result<()>> {
- self.api_token = Task::ready(api_token.clone()).shared();
- store_api_token_in_keychain(api_token, cx)
- }
-
pub(crate) fn request_prediction(
&self,
EditPredictionModelInput {
@@ -48,7 +42,10 @@ impl Mercury {
}: EditPredictionModelInput,
cx: &mut App,
) -> Task<Result<Option<EditPredictionResult>>> {
- let Some(api_token) = self.api_token.clone().now_or_never().flatten() else {
+ self.api_token.update(cx, |key_state, cx| {
+ _ = key_state.load_if_needed(MERCURY_CREDENTIALS_URL, |s| s, cx);
+ });
+ let Some(api_token) = self.api_token.read(cx).key(&MERCURY_CREDENTIALS_URL) else {
return Task::ready(Ok(None));
};
let full_path: Arc<Path> = snapshot
@@ -299,45 +296,16 @@ fn push_delimited(prompt: &mut String, delimiters: Range<&str>, cb: impl FnOnce(
prompt.push_str(delimiters.end);
}
-pub const MERCURY_CREDENTIALS_URL: &str = "https://api.inceptionlabs.ai/v1/edit/completions";
+pub const MERCURY_CREDENTIALS_URL: SharedString =
+ SharedString::new_static("https://api.inceptionlabs.ai/v1/edit/completions");
pub const MERCURY_CREDENTIALS_USERNAME: &str = "mercury-api-token";
+pub static MERCURY_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("MERCURY_AI_TOKEN");
+pub static MERCURY_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
-pub fn load_api_token(cx: &App) -> Task<Option<String>> {
- if let Some(api_token) = std::env::var("MERCURY_AI_TOKEN")
- .ok()
- .filter(|value| !value.is_empty())
- {
- return Task::ready(Some(api_token));
- }
- let credentials_provider = <dyn CredentialsProvider>::global(cx);
- cx.spawn(async move |cx| {
- let (_, credentials) = credentials_provider
- .read_credentials(MERCURY_CREDENTIALS_URL, &cx)
- .await
- .ok()??;
- String::from_utf8(credentials).ok()
- })
-}
-
-fn store_api_token_in_keychain(api_token: Option<String>, cx: &App) -> Task<Result<()>> {
- let credentials_provider = <dyn CredentialsProvider>::global(cx);
-
- cx.spawn(async move |cx| {
- if let Some(api_token) = api_token {
- credentials_provider
- .write_credentials(
- MERCURY_CREDENTIALS_URL,
- MERCURY_CREDENTIALS_USERNAME,
- api_token.as_bytes(),
- cx,
- )
- .await
- .context("Failed to save Mercury API token to system keychain")
- } else {
- credentials_provider
- .delete_credentials(MERCURY_CREDENTIALS_URL, cx)
- .await
- .context("Failed to delete Mercury API token from system keychain")
- }
- })
+pub fn mercury_api_token(cx: &mut App) -> Entity<ApiKeyState> {
+ MERCURY_API_KEY
+ .get_or_init(|| {
+ cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()))
+ })
+ .clone()
}
@@ -1,11 +1,11 @@
-use anyhow::{Context as _, Result};
-use credentials_provider::CredentialsProvider;
-use futures::{AsyncReadExt as _, FutureExt, future::Shared};
+use anyhow::Result;
+use futures::AsyncReadExt as _;
use gpui::{
- App, AppContext as _, Task,
+ App, AppContext as _, Entity, SharedString, Task,
http_client::{self, AsyncBody, Method},
};
use language::{Point, ToOffset as _};
+use language_model::{ApiKeyState, EnvVar, env_var};
use lsp::DiagnosticSeverity;
use serde::{Deserialize, Serialize};
use std::{
@@ -20,30 +20,28 @@ use crate::{EditPredictionId, EditPredictionModelInput, prediction::EditPredicti
const SWEEP_API_URL: &str = "https://autocomplete.sweep.dev/backend/next_edit_autocomplete";
pub struct SweepAi {
- pub api_token: Shared<Task<Option<String>>>,
+ pub api_token: Entity<ApiKeyState>,
pub debug_info: Arc<str>,
}
impl SweepAi {
- pub fn new(cx: &App) -> Self {
+ pub fn new(cx: &mut App) -> Self {
SweepAi {
- api_token: load_api_token(cx).shared(),
+ api_token: sweep_api_token(cx),
debug_info: debug_info(cx),
}
}
- pub fn set_api_token(&mut self, api_token: Option<String>, cx: &mut App) -> Task<Result<()>> {
- self.api_token = Task::ready(api_token.clone()).shared();
- store_api_token_in_keychain(api_token, cx)
- }
-
pub fn request_prediction_with_sweep(
&self,
inputs: EditPredictionModelInput,
cx: &mut App,
) -> Task<Result<Option<EditPredictionResult>>> {
let debug_info = self.debug_info.clone();
- let Some(api_token) = self.api_token.clone().now_or_never().flatten() else {
+ self.api_token.update(cx, |key_state, cx| {
+ _ = key_state.load_if_needed(SWEEP_CREDENTIALS_URL, |s| s, cx);
+ });
+ let Some(api_token) = self.api_token.read(cx).key(&SWEEP_CREDENTIALS_URL) else {
return Task::ready(Ok(None));
};
let full_path: Arc<Path> = inputs
@@ -270,47 +268,18 @@ impl SweepAi {
}
}
-pub const SWEEP_CREDENTIALS_URL: &str = "https://autocomplete.sweep.dev";
+pub const SWEEP_CREDENTIALS_URL: SharedString =
+ SharedString::new_static("https://autocomplete.sweep.dev");
pub const SWEEP_CREDENTIALS_USERNAME: &str = "sweep-api-token";
+pub static SWEEP_AI_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("SWEEP_AI_TOKEN");
+pub static SWEEP_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
-pub fn load_api_token(cx: &App) -> Task<Option<String>> {
- if let Some(api_token) = std::env::var("SWEEP_AI_TOKEN")
- .ok()
- .filter(|value| !value.is_empty())
- {
- return Task::ready(Some(api_token));
- }
- let credentials_provider = <dyn CredentialsProvider>::global(cx);
- cx.spawn(async move |cx| {
- let (_, credentials) = credentials_provider
- .read_credentials(SWEEP_CREDENTIALS_URL, &cx)
- .await
- .ok()??;
- String::from_utf8(credentials).ok()
- })
-}
-
-fn store_api_token_in_keychain(api_token: Option<String>, cx: &App) -> Task<Result<()>> {
- let credentials_provider = <dyn CredentialsProvider>::global(cx);
-
- cx.spawn(async move |cx| {
- if let Some(api_token) = api_token {
- credentials_provider
- .write_credentials(
- SWEEP_CREDENTIALS_URL,
- SWEEP_CREDENTIALS_USERNAME,
- api_token.as_bytes(),
- cx,
- )
- .await
- .context("Failed to save Sweep API token to system keychain")
- } else {
- credentials_provider
- .delete_credentials(SWEEP_CREDENTIALS_URL, cx)
- .await
- .context("Failed to delete Sweep API token from system keychain")
- }
- })
+pub fn sweep_api_token(cx: &mut App) -> Entity<ApiKeyState> {
+ SWEEP_API_KEY
+ .get_or_init(|| {
+ cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()))
+ })
+ .clone()
}
#[derive(Debug, Clone, Serialize)]
@@ -100,7 +100,7 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate {
) -> bool {
let store = self.store.read(cx);
if store.edit_prediction_model == EditPredictionModel::Sweep {
- store.has_sweep_api_token()
+ store.has_sweep_api_token(cx)
} else {
true
}
@@ -20,8 +20,8 @@ cloud_llm_client.workspace = true
codestral.workspace = true
command_palette_hooks.workspace = true
copilot.workspace = true
-edit_prediction.workspace = true
edit_prediction_types.workspace = true
+edit_prediction.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
@@ -41,7 +41,6 @@ telemetry.workspace = true
text.workspace = true
theme.workspace = true
ui.workspace = true
-ui_input.workspace = true
util.workspace = true
workspace.workspace = true
zed_actions.workspace = true
@@ -3,7 +3,9 @@ use client::{Client, UserStore, zed_urls};
use cloud_llm_client::UsageLimit;
use codestral::CodestralEditPredictionDelegate;
use copilot::{Copilot, Status};
-use edit_prediction::{MercuryFeatureFlag, SweepFeatureFlag, Zeta2FeatureFlag};
+use edit_prediction::{
+ EditPredictionStore, MercuryFeatureFlag, SweepFeatureFlag, Zeta2FeatureFlag,
+};
use edit_prediction_types::EditPredictionDelegateHandle;
use editor::{
Editor, MultiBufferOffset, SelectionEffects, actions::ShowEditPrediction, scroll::Autoscroll,
@@ -42,12 +44,9 @@ use workspace::{
StatusItemView, Toast, Workspace, create_and_open_local_file, item::ItemHandle,
notifications::NotificationId,
};
-use zed_actions::OpenBrowser;
+use zed_actions::{OpenBrowser, OpenSettingsAt};
-use crate::{
- ExternalProviderApiKeyModal, RatePredictions,
- rate_prediction_modal::PredictEditsRatePredictionsFeatureFlag,
-};
+use crate::{RatePredictions, rate_prediction_modal::PredictEditsRatePredictionsFeatureFlag};
actions!(
edit_prediction,
@@ -248,45 +247,21 @@ impl Render for EditPredictionButton {
EditPredictionProvider::Codestral => {
let enabled = self.editor_enabled.unwrap_or(true);
let has_api_key = CodestralEditPredictionDelegate::has_api_key(cx);
- let fs = self.fs.clone();
let this = cx.weak_entity();
+ let tooltip_meta = if has_api_key {
+ "Powered by Codestral"
+ } else {
+ "Missing API key for Codestral"
+ };
+
div().child(
PopoverMenu::new("codestral")
.menu(move |window, cx| {
- if has_api_key {
- this.update(cx, |this, cx| {
- this.build_codestral_context_menu(window, cx)
- })
- .ok()
- } else {
- Some(ContextMenu::build(window, cx, |menu, _, _| {
- let fs = fs.clone();
-
- menu.entry(
- "Configure Codestral API Key",
- None,
- move |window, cx| {
- window.dispatch_action(
- zed_actions::agent::OpenSettings.boxed_clone(),
- cx,
- );
- },
- )
- .separator()
- .entry(
- "Use Zed AI instead",
- None,
- move |_, cx| {
- set_completion_provider(
- fs.clone(),
- cx,
- EditPredictionProvider::Zed,
- )
- },
- )
- }))
- }
+ this.update(cx, |this, cx| {
+ this.build_codestral_context_menu(window, cx)
+ })
+ .ok()
})
.anchor(Corner::BottomRight)
.trigger_with_tooltip(
@@ -304,7 +279,14 @@ impl Render for EditPredictionButton {
cx.theme().colors().status_bar_background,
))
}),
- move |_window, cx| Tooltip::for_action("Codestral", &ToggleMenu, cx),
+ move |_window, cx| {
+ Tooltip::with_meta(
+ "Edit Prediction",
+ Some(&ToggleMenu),
+ tooltip_meta,
+ cx,
+ )
+ },
)
.with_handle(self.popover_menu_handle.clone()),
)
@@ -313,6 +295,7 @@ impl Render for EditPredictionButton {
let enabled = self.editor_enabled.unwrap_or(true);
let ep_icon;
+ let tooltip_meta;
let mut missing_token = false;
match provider {
@@ -320,15 +303,25 @@ impl Render for EditPredictionButton {
EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME,
) => {
ep_icon = IconName::SweepAi;
+ tooltip_meta = if missing_token {
+ "Missing API key for Sweep"
+ } else {
+ "Powered by Sweep"
+ };
missing_token = edit_prediction::EditPredictionStore::try_global(cx)
- .is_some_and(|ep_store| !ep_store.read(cx).has_sweep_api_token());
+ .is_some_and(|ep_store| !ep_store.read(cx).has_sweep_api_token(cx));
}
EditPredictionProvider::Experimental(
EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME,
) => {
ep_icon = IconName::Inception;
missing_token = edit_prediction::EditPredictionStore::try_global(cx)
- .is_some_and(|ep_store| !ep_store.read(cx).has_mercury_api_token());
+ .is_some_and(|ep_store| !ep_store.read(cx).has_mercury_api_token(cx));
+ tooltip_meta = if missing_token {
+ "Missing API key for Mercury"
+ } else {
+ "Powered by Mercury"
+ };
}
_ => {
ep_icon = if enabled {
@@ -336,6 +329,7 @@ impl Render for EditPredictionButton {
} else {
IconName::ZedPredictDisabled
};
+ tooltip_meta = "Powered by Zeta"
}
};
@@ -400,33 +394,26 @@ impl Render for EditPredictionButton {
})
.when(!self.popover_menu_handle.is_deployed(), |element| {
let user = user.clone();
+
element.tooltip(move |_window, cx| {
- if enabled {
+ let description = if enabled {
if show_editor_predictions {
- Tooltip::for_action("Edit Prediction", &ToggleMenu, cx)
+ tooltip_meta
} else if user.is_none() {
- Tooltip::with_meta(
- "Edit Prediction",
- Some(&ToggleMenu),
- "Sign In To Use",
- cx,
- )
+ "Sign In To Use"
} else {
- Tooltip::with_meta(
- "Edit Prediction",
- Some(&ToggleMenu),
- "Hidden For This File",
- cx,
- )
+ "Hidden For This File"
}
} else {
- Tooltip::with_meta(
- "Edit Prediction",
- Some(&ToggleMenu),
- "Disabled For This File",
- cx,
- )
- }
+ "Disabled For This File"
+ };
+
+ Tooltip::with_meta(
+ "Edit Prediction",
+ Some(&ToggleMenu),
+ description,
+ cx,
+ )
})
});
@@ -519,6 +506,12 @@ impl EditPredictionButton {
providers.push(EditPredictionProvider::Zed);
+ if cx.has_flag::<Zeta2FeatureFlag>() {
+ providers.push(EditPredictionProvider::Experimental(
+ EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME,
+ ));
+ }
+
if let Some(copilot) = Copilot::global(cx) {
if matches!(copilot.read(cx).status(), Status::Authorized) {
providers.push(EditPredictionProvider::Copilot);
@@ -537,24 +530,28 @@ impl EditPredictionButton {
providers.push(EditPredictionProvider::Codestral);
}
- if cx.has_flag::<SweepFeatureFlag>() {
+ let ep_store = EditPredictionStore::try_global(cx);
+
+ if cx.has_flag::<SweepFeatureFlag>()
+ && ep_store
+ .as_ref()
+ .is_some_and(|ep_store| ep_store.read(cx).has_sweep_api_token(cx))
+ {
providers.push(EditPredictionProvider::Experimental(
EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME,
));
}
- if cx.has_flag::<MercuryFeatureFlag>() {
+ if cx.has_flag::<MercuryFeatureFlag>()
+ && ep_store
+ .as_ref()
+ .is_some_and(|ep_store| ep_store.read(cx).has_mercury_api_token(cx))
+ {
providers.push(EditPredictionProvider::Experimental(
EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME,
));
}
- if cx.has_flag::<Zeta2FeatureFlag>() {
- providers.push(EditPredictionProvider::Experimental(
- EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME,
- ));
- }
-
providers
}
@@ -562,13 +559,10 @@ impl EditPredictionButton {
&self,
mut menu: ContextMenu,
current_provider: EditPredictionProvider,
- cx: &App,
+ cx: &mut App,
) -> ContextMenu {
let available_providers = self.get_available_providers(cx);
- const ZED_AI_CALLOUT: &str =
- "Zed's edit prediction is powered by Zeta, an open-source, dataset mode.";
-
let providers: Vec<_> = available_providers
.into_iter()
.filter(|p| *p != EditPredictionProvider::None)
@@ -581,153 +575,32 @@ impl EditPredictionButton {
let is_current = provider == current_provider;
let fs = self.fs.clone();
- menu = match provider {
- EditPredictionProvider::Zed => menu.item(
- ContextMenuEntry::new("Zed AI")
- .toggleable(IconPosition::Start, is_current)
- .documentation_aside(
- DocumentationSide::Left,
- DocumentationEdge::Bottom,
- |_| Label::new(ZED_AI_CALLOUT).into_any_element(),
- )
- .handler(move |_, cx| {
- set_completion_provider(fs.clone(), cx, provider);
- }),
- ),
- EditPredictionProvider::Copilot => menu.item(
- ContextMenuEntry::new("GitHub Copilot")
- .toggleable(IconPosition::Start, is_current)
- .handler(move |_, cx| {
- set_completion_provider(fs.clone(), cx, provider);
- }),
- ),
- EditPredictionProvider::Supermaven => menu.item(
- ContextMenuEntry::new("Supermaven")
- .toggleable(IconPosition::Start, is_current)
- .handler(move |_, cx| {
- set_completion_provider(fs.clone(), cx, provider);
- }),
- ),
- EditPredictionProvider::Codestral => menu.item(
- ContextMenuEntry::new("Codestral")
- .toggleable(IconPosition::Start, is_current)
- .handler(move |_, cx| {
- set_completion_provider(fs.clone(), cx, provider);
- }),
- ),
+ let name = match provider {
+ EditPredictionProvider::Zed => "Zed AI",
+ EditPredictionProvider::Copilot => "GitHub Copilot",
+ EditPredictionProvider::Supermaven => "Supermaven",
+ EditPredictionProvider::Codestral => "Codestral",
EditPredictionProvider::Experimental(
EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME,
- ) => {
- let has_api_token = edit_prediction::EditPredictionStore::try_global(cx)
- .map_or(false, |ep_store| ep_store.read(cx).has_sweep_api_token());
-
- let should_open_modal = !has_api_token || is_current;
-
- let entry = if has_api_token {
- ContextMenuEntry::new("Sweep")
- .toggleable(IconPosition::Start, is_current)
- } else {
- ContextMenuEntry::new("Sweep")
- .icon(IconName::XCircle)
- .icon_color(Color::Error)
- .documentation_aside(
- DocumentationSide::Left,
- DocumentationEdge::Bottom,
- |_| {
- Label::new("Click to configure your Sweep API token")
- .into_any_element()
- },
- )
- };
-
- let entry = entry.handler(move |window, cx| {
- if should_open_modal {
- if let Some(workspace) = window.root::<Workspace>().flatten() {
- workspace.update(cx, |workspace, cx| {
- workspace.toggle_modal(window, cx, |window, cx| {
- ExternalProviderApiKeyModal::new(
- window,
- cx,
- |api_key, store, cx| {
- store
- .sweep_ai
- .set_api_token(api_key, cx)
- .detach_and_log_err(cx);
- },
- )
- });
- });
- };
- } else {
- set_completion_provider(fs.clone(), cx, provider);
- }
- });
-
- menu.item(entry)
- }
+ ) => "Sweep",
EditPredictionProvider::Experimental(
EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME,
- ) => {
- let has_api_token = edit_prediction::EditPredictionStore::try_global(cx)
- .map_or(false, |ep_store| ep_store.read(cx).has_mercury_api_token());
-
- let should_open_modal = !has_api_token || is_current;
-
- let entry = if has_api_token {
- ContextMenuEntry::new("Mercury")
- .toggleable(IconPosition::Start, is_current)
- } else {
- ContextMenuEntry::new("Mercury")
- .icon(IconName::XCircle)
- .icon_color(Color::Error)
- .documentation_aside(
- DocumentationSide::Left,
- DocumentationEdge::Bottom,
- |_| {
- Label::new("Click to configure your Mercury API token")
- .into_any_element()
- },
- )
- };
-
- let entry = entry.handler(move |window, cx| {
- if should_open_modal {
- if let Some(workspace) = window.root::<Workspace>().flatten() {
- workspace.update(cx, |workspace, cx| {
- workspace.toggle_modal(window, cx, |window, cx| {
- ExternalProviderApiKeyModal::new(
- window,
- cx,
- |api_key, store, cx| {
- store
- .mercury
- .set_api_token(api_key, cx)
- .detach_and_log_err(cx);
- },
- )
- });
- });
- };
- } else {
- set_completion_provider(fs.clone(), cx, provider);
- }
- });
-
- menu.item(entry)
- }
+ ) => "Mercury",
EditPredictionProvider::Experimental(
EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME,
- ) => menu.item(
- ContextMenuEntry::new("Zeta2")
- .toggleable(IconPosition::Start, is_current)
- .handler(move |_, cx| {
- set_completion_provider(fs.clone(), cx, provider);
- }),
- ),
+ ) => "Zeta2",
EditPredictionProvider::None | EditPredictionProvider::Experimental(_) => {
continue;
}
};
+
+ menu = menu.item(
+ ContextMenuEntry::new(name)
+ .toggleable(IconPosition::Start, is_current)
+ .handler(move |_, cx| {
+ set_completion_provider(fs.clone(), cx, provider);
+ }),
+ )
}
}
@@ -832,14 +705,7 @@ impl EditPredictionButton {
let subtle_mode = matches!(current_mode, EditPredictionsMode::Subtle);
let eager_mode = matches!(current_mode, EditPredictionsMode::Eager);
- if matches!(
- provider,
- EditPredictionProvider::Zed
- | EditPredictionProvider::Copilot
- | EditPredictionProvider::Supermaven
- | EditPredictionProvider::Codestral
- ) {
- menu = menu
+ menu = menu
.separator()
.header("Display Modes")
.item(
@@ -868,104 +734,111 @@ impl EditPredictionButton {
}
}),
);
- }
menu = menu.separator().header("Privacy");
- if let Some(provider) = &self.edit_prediction_provider {
- let data_collection = provider.data_collection_state(cx);
-
- if data_collection.is_supported() {
- let provider = provider.clone();
- let enabled = data_collection.is_enabled();
- let is_open_source = data_collection.is_project_open_source();
- let is_collecting = data_collection.is_enabled();
- let (icon_name, icon_color) = if is_open_source && is_collecting {
- (IconName::Check, Color::Success)
- } else {
- (IconName::Check, Color::Accent)
- };
-
- menu = menu.item(
- ContextMenuEntry::new("Training Data Collection")
- .toggleable(IconPosition::Start, data_collection.is_enabled())
- .icon(icon_name)
- .icon_color(icon_color)
- .documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, move |cx| {
- let (msg, label_color, icon_name, icon_color) = match (is_open_source, is_collecting) {
- (true, true) => (
- "Project identified as open source, and you're sharing data.",
- Color::Default,
- IconName::Check,
- Color::Success,
- ),
- (true, false) => (
- "Project identified as open source, but you're not sharing data.",
- Color::Muted,
- IconName::Close,
- Color::Muted,
- ),
- (false, true) => (
- "Project not identified as open source. No data captured.",
- Color::Muted,
- IconName::Close,
- Color::Muted,
- ),
- (false, false) => (
- "Project not identified as open source, and setting turned off.",
- Color::Muted,
- IconName::Close,
- Color::Muted,
- ),
- };
- v_flex()
- .gap_2()
- .child(
- Label::new(indoc!{
- "Help us improve our open dataset model by sharing data from open source repositories. \
- Zed must detect a license file in your repo for this setting to take effect. \
- Files with sensitive data and secrets are excluded by default."
- })
- )
- .child(
- h_flex()
- .items_start()
- .pt_2()
- .pr_1()
- .flex_1()
- .gap_1p5()
- .border_t_1()
- .border_color(cx.theme().colors().border_variant)
- .child(h_flex().flex_shrink_0().h(line_height).child(Icon::new(icon_name).size(IconSize::XSmall).color(icon_color)))
- .child(div().child(msg).w_full().text_sm().text_color(label_color.color(cx)))
- )
- .into_any_element()
- })
- .handler(move |_, cx| {
- provider.toggle_data_collection(cx);
-
- if !enabled {
- telemetry::event!(
- "Data Collection Enabled",
- source = "Edit Prediction Status Menu"
- );
- } else {
- telemetry::event!(
- "Data Collection Disabled",
- source = "Edit Prediction Status Menu"
- );
- }
- })
- );
+ if matches!(
+ provider,
+ EditPredictionProvider::Zed
+ | EditPredictionProvider::Experimental(
+ EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME,
+ )
+ ) {
+ if let Some(provider) = &self.edit_prediction_provider {
+ let data_collection = provider.data_collection_state(cx);
+
+ if data_collection.is_supported() {
+ let provider = provider.clone();
+ let enabled = data_collection.is_enabled();
+ let is_open_source = data_collection.is_project_open_source();
+ let is_collecting = data_collection.is_enabled();
+ let (icon_name, icon_color) = if is_open_source && is_collecting {
+ (IconName::Check, Color::Success)
+ } else {
+ (IconName::Check, Color::Accent)
+ };
- if is_collecting && !is_open_source {
menu = menu.item(
- ContextMenuEntry::new("No data captured.")
- .disabled(true)
- .icon(IconName::Close)
- .icon_color(Color::Error)
- .icon_size(IconSize::Small),
+ ContextMenuEntry::new("Training Data Collection")
+ .toggleable(IconPosition::Start, data_collection.is_enabled())
+ .icon(icon_name)
+ .icon_color(icon_color)
+ .documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, move |cx| {
+ let (msg, label_color, icon_name, icon_color) = match (is_open_source, is_collecting) {
+ (true, true) => (
+ "Project identified as open source, and you're sharing data.",
+ Color::Default,
+ IconName::Check,
+ Color::Success,
+ ),
+ (true, false) => (
+ "Project identified as open source, but you're not sharing data.",
+ Color::Muted,
+ IconName::Close,
+ Color::Muted,
+ ),
+ (false, true) => (
+ "Project not identified as open source. No data captured.",
+ Color::Muted,
+ IconName::Close,
+ Color::Muted,
+ ),
+ (false, false) => (
+ "Project not identified as open source, and setting turned off.",
+ Color::Muted,
+ IconName::Close,
+ Color::Muted,
+ ),
+ };
+ v_flex()
+ .gap_2()
+ .child(
+ Label::new(indoc!{
+ "Help us improve our open dataset model by sharing data from open source repositories. \
+ Zed must detect a license file in your repo for this setting to take effect. \
+ Files with sensitive data and secrets are excluded by default."
+ })
+ )
+ .child(
+ h_flex()
+ .items_start()
+ .pt_2()
+ .pr_1()
+ .flex_1()
+ .gap_1p5()
+ .border_t_1()
+ .border_color(cx.theme().colors().border_variant)
+ .child(h_flex().flex_shrink_0().h(line_height).child(Icon::new(icon_name).size(IconSize::XSmall).color(icon_color)))
+ .child(div().child(msg).w_full().text_sm().text_color(label_color.color(cx)))
+ )
+ .into_any_element()
+ })
+ .handler(move |_, cx| {
+ provider.toggle_data_collection(cx);
+
+ if !enabled {
+ telemetry::event!(
+ "Data Collection Enabled",
+ source = "Edit Prediction Status Menu"
+ );
+ } else {
+ telemetry::event!(
+ "Data Collection Disabled",
+ source = "Edit Prediction Status Menu"
+ );
+ }
+ })
);
+
+ if is_collecting && !is_open_source {
+ menu = menu.item(
+ ContextMenuEntry::new("No data captured.")
+ .disabled(true)
+ .icon(IconName::Close)
+ .icon_color(Color::Error)
+ .icon_size(IconSize::Small),
+ );
+ }
}
}
}
@@ -1087,10 +960,7 @@ impl EditPredictionButton {
let menu =
self.add_provider_switching_section(menu, EditPredictionProvider::Codestral, cx);
- menu.separator()
- .entry("Configure Codestral API Key", None, move |window, cx| {
- window.dispatch_action(zed_actions::agent::OpenSettings.boxed_clone(), cx);
- })
+ menu
})
}
@@ -1210,6 +1080,22 @@ impl EditPredictionButton {
}
menu = self.add_provider_switching_section(menu, provider, cx);
+ menu = menu.separator().item(
+ ContextMenuEntry::new("Configure Providers")
+ .icon(IconName::Settings)
+ .icon_position(IconPosition::Start)
+ .icon_color(Color::Muted)
+ .handler(move |window, cx| {
+ window.dispatch_action(
+ OpenSettingsAt {
+ path: "edit_predictions.providers".to_string(),
+ }
+ .boxed_clone(),
+ cx,
+ );
+ }),
+ );
+
menu
})
}
@@ -1,6 +1,5 @@
mod edit_prediction_button;
mod edit_prediction_context_view;
-mod external_provider_api_token_modal;
mod rate_prediction_modal;
use std::any::{Any as _, TypeId};
@@ -17,7 +16,6 @@ use ui::{App, prelude::*};
use workspace::{SplitDirection, Workspace};
pub use edit_prediction_button::{EditPredictionButton, ToggleMenu};
-pub use external_provider_api_token_modal::ExternalProviderApiKeyModal;
use crate::rate_prediction_modal::PredictEditsRatePredictionsFeatureFlag;
@@ -1,86 +0,0 @@
-use edit_prediction::EditPredictionStore;
-use gpui::{
- DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render,
-};
-use ui::{Button, ButtonStyle, Clickable, Headline, HeadlineSize, prelude::*};
-use ui_input::InputField;
-use workspace::ModalView;
-
-pub struct ExternalProviderApiKeyModal {
- api_key_input: Entity<InputField>,
- focus_handle: FocusHandle,
- on_confirm: Box<dyn Fn(Option<String>, &mut EditPredictionStore, &mut App)>,
-}
-
-impl ExternalProviderApiKeyModal {
- pub fn new(
- window: &mut Window,
- cx: &mut Context<Self>,
- on_confirm: impl Fn(Option<String>, &mut EditPredictionStore, &mut App) + 'static,
- ) -> Self {
- let api_key_input = cx.new(|cx| InputField::new(window, cx, "Enter your API key"));
-
- Self {
- api_key_input,
- focus_handle: cx.focus_handle(),
- on_confirm: Box::new(on_confirm),
- }
- }
-
- fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
- cx.emit(DismissEvent);
- }
-
- fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
- let api_key = self.api_key_input.read(cx).text(cx);
- let api_key = (!api_key.trim().is_empty()).then_some(api_key);
-
- if let Some(ep_store) = EditPredictionStore::try_global(cx) {
- ep_store.update(cx, |ep_store, cx| (self.on_confirm)(api_key, ep_store, cx))
- }
-
- cx.emit(DismissEvent);
- }
-}
-
-impl EventEmitter<DismissEvent> for ExternalProviderApiKeyModal {}
-
-impl ModalView for ExternalProviderApiKeyModal {}
-
-impl Focusable for ExternalProviderApiKeyModal {
- fn focus_handle(&self, _cx: &App) -> FocusHandle {
- self.focus_handle.clone()
- }
-}
-
-impl Render for ExternalProviderApiKeyModal {
- fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- v_flex()
- .key_context("ExternalApiKeyModal")
- .on_action(cx.listener(Self::cancel))
- .on_action(cx.listener(Self::confirm))
- .elevation_2(cx)
- .w(px(400.))
- .p_4()
- .gap_3()
- .child(Headline::new("API Token").size(HeadlineSize::Small))
- .child(self.api_key_input.clone())
- .child(
- h_flex()
- .justify_end()
- .gap_2()
- .child(Button::new("cancel", "Cancel").on_click(cx.listener(
- |_, _, _window, cx| {
- cx.emit(DismissEvent);
- },
- )))
- .child(
- Button::new("save", "Save")
- .style(ButtonStyle::Filled)
- .on_click(cx.listener(|this, _, window, cx| {
- this.confirm(&menu::Confirm, window, cx);
- })),
- ),
- )
- }
-}
@@ -18,6 +18,7 @@ test-support = []
[dependencies]
anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
+credentials_provider.workspace = true
base64.workspace = true
client.workspace = true
cloud_api_types.workspace = true
@@ -41,6 +42,7 @@ smol.workspace = true
telemetry_events.workspace = true
thiserror.workspace = true
util.workspace = true
+zed_env_vars.workspace = true
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }
@@ -2,7 +2,6 @@ use anyhow::{Result, anyhow};
use credentials_provider::CredentialsProvider;
use futures::{FutureExt, future};
use gpui::{AsyncApp, Context, SharedString, Task};
-use language_model::AuthenticateError;
use std::{
fmt::{Display, Formatter},
sync::Arc,
@@ -10,13 +9,16 @@ use std::{
use util::ResultExt as _;
use zed_env_vars::EnvVar;
+use crate::AuthenticateError;
+
/// Manages a single API key for a language model provider. API keys either come from environment
/// variables or the system keychain.
///
/// Keys from the system keychain are associated with a provider URL, and this ensures that they are
/// only used with that URL.
pub struct ApiKeyState {
- url: SharedString,
+ pub url: SharedString,
+ env_var: EnvVar,
load_status: LoadStatus,
load_task: Option<future::Shared<Task<()>>>,
}
@@ -35,9 +37,10 @@ pub struct ApiKey {
}
impl ApiKeyState {
- pub fn new(url: SharedString) -> Self {
+ pub fn new(url: SharedString, env_var: EnvVar) -> Self {
Self {
url,
+ env_var,
load_status: LoadStatus::NotPresent,
load_task: None,
}
@@ -47,6 +50,10 @@ impl ApiKeyState {
matches!(self.load_status, LoadStatus::Loaded { .. })
}
+ pub fn env_var_name(&self) -> &SharedString {
+ &self.env_var.name
+ }
+
pub fn is_from_env_var(&self) -> bool {
match &self.load_status {
LoadStatus::Loaded(ApiKey {
@@ -136,14 +143,13 @@ impl ApiKeyState {
pub fn handle_url_change<Ent: 'static>(
&mut self,
url: SharedString,
- env_var: &EnvVar,
get_this: impl Fn(&mut Ent) -> &mut Self + Clone + 'static,
cx: &mut Context<Ent>,
) {
if url != self.url {
if !self.is_from_env_var() {
// loading will continue even though this result task is dropped
- let _task = self.load_if_needed(url, env_var, get_this, cx);
+ let _task = self.load_if_needed(url, get_this, cx);
}
}
}
@@ -156,7 +162,6 @@ impl ApiKeyState {
pub fn load_if_needed<Ent: 'static>(
&mut self,
url: SharedString,
- env_var: &EnvVar,
get_this: impl Fn(&mut Ent) -> &mut Self + Clone + 'static,
cx: &mut Context<Ent>,
) -> Task<Result<(), AuthenticateError>> {
@@ -166,10 +171,10 @@ impl ApiKeyState {
return Task::ready(Ok(()));
}
- if let Some(key) = &env_var.value
+ if let Some(key) = &self.env_var.value
&& !key.is_empty()
{
- let api_key = ApiKey::from_env(env_var.name.clone(), key);
+ let api_key = ApiKey::from_env(self.env_var.name.clone(), key);
self.url = url;
self.load_status = LoadStatus::Loaded(api_key);
self.load_task = None;
@@ -1,3 +1,4 @@
+mod api_key;
mod model;
mod rate_limiter;
mod registry;
@@ -30,6 +31,7 @@ use std::{fmt, io};
use thiserror::Error;
use util::serde::is_default;
+pub use crate::api_key::{ApiKey, ApiKeyState};
pub use crate::model::*;
pub use crate::rate_limiter::*;
pub use crate::registry::*;
@@ -37,6 +39,7 @@ pub use crate::request::*;
pub use crate::role::*;
pub use crate::telemetry::*;
pub use crate::tool_schema::LanguageModelToolSchemaFormat;
+pub use zed_env_vars::{EnvVar, env_var};
pub const ANTHROPIC_PROVIDER_ID: LanguageModelProviderId =
LanguageModelProviderId::new("anthropic");
@@ -60,7 +60,6 @@ ui_input.workspace = true
util.workspace = true
vercel = { workspace = true, features = ["schemars"] }
x_ai = { workspace = true, features = ["schemars"] }
-zed_env_vars.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }
@@ -7,10 +7,8 @@ use gpui::{App, Context, Entity};
use language_model::{LanguageModelProviderId, LanguageModelRegistry};
use provider::deepseek::DeepSeekLanguageModelProvider;
-mod api_key;
pub mod provider;
mod settings;
-pub mod ui;
use crate::provider::anthropic::AnthropicLanguageModelProvider;
use crate::provider::bedrock::BedrockLanguageModelProvider;
@@ -8,25 +8,21 @@ use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture, stream::B
use gpui::{AnyView, App, AsyncApp, Context, Entity, Task};
use http_client::HttpClient;
use language_model::{
- AuthenticateError, ConfigurationViewTargetAgent, LanguageModel,
- LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelId,
- LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
- LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
- LanguageModelToolResultContent, MessageContent, RateLimiter, Role,
+ ApiKeyState, AuthenticateError, ConfigurationViewTargetAgent, EnvVar, LanguageModel,
+ LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelCompletionEvent,
+ LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
+ LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
+ LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent,
+ RateLimiter, Role, StopReason, env_var,
};
-use language_model::{LanguageModelCompletionEvent, LanguageModelToolUse, StopReason};
use settings::{Settings, SettingsStore};
use std::pin::Pin;
use std::str::FromStr;
use std::sync::{Arc, LazyLock};
use strum::IntoEnumIterator;
-use ui::{List, prelude::*};
+use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
use ui_input::InputField;
use util::ResultExt;
-use zed_env_vars::{EnvVar, env_var};
-
-use crate::api_key::ApiKeyState;
-use crate::ui::{ConfiguredApiCard, InstructionListItem};
pub use settings::AnthropicAvailableModel as AvailableModel;
@@ -65,12 +61,8 @@ impl State {
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
let api_url = AnthropicLanguageModelProvider::api_url(cx);
- self.api_key_state.load_if_needed(
- api_url,
- &API_KEY_ENV_VAR,
- |this| &mut this.api_key_state,
- cx,
- )
+ self.api_key_state
+ .load_if_needed(api_url, |this| &mut this.api_key_state, cx)
}
}
@@ -79,17 +71,13 @@ impl AnthropicLanguageModelProvider {
let state = cx.new(|cx| {
cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
let api_url = Self::api_url(cx);
- this.api_key_state.handle_url_change(
- api_url,
- &API_KEY_ENV_VAR,
- |this| &mut this.api_key_state,
- cx,
- );
+ this.api_key_state
+ .handle_url_change(api_url, |this| &mut this.api_key_state, cx);
cx.notify();
})
.detach();
State {
- api_key_state: ApiKeyState::new(Self::api_url(cx)),
+ api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
}
});
@@ -937,14 +925,12 @@ impl Render for ConfigurationView {
.child(
List::new()
.child(
- InstructionListItem::new(
- "Create one by visiting",
- Some("Anthropic's settings"),
- Some("https://console.anthropic.com/settings/keys")
- )
+ ListBulletItem::new("")
+ .child(Label::new("Create one by visiting"))
+ .child(ButtonLink::new("Anthropic's settings", "https://console.anthropic.com/settings/keys"))
)
.child(
- InstructionListItem::text_only("Paste your API key below and hit enter to start using the agent")
+ ListBulletItem::new("Paste your API key below and hit enter to start using the agent")
)
)
.child(self.api_key_editor.clone())
@@ -953,7 +939,8 @@ impl Render for ConfigurationView {
format!("You can also assign the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed."),
)
.size(LabelSize::Small)
- .color(Color::Muted),
+ .color(Color::Muted)
+ .mt_0p5(),
)
.into_any_element()
} else {
@@ -2,7 +2,6 @@ use std::pin::Pin;
use std::str::FromStr;
use std::sync::Arc;
-use crate::ui::{ConfiguredApiCard, InstructionListItem};
use anyhow::{Context as _, Result, anyhow};
use aws_config::stalled_stream_protection::StalledStreamProtectionConfig;
use aws_config::{BehaviorVersion, Region};
@@ -44,7 +43,7 @@ use serde_json::Value;
use settings::{BedrockAvailableModel as AvailableModel, Settings, SettingsStore};
use smol::lock::OnceCell;
use strum::{EnumIter, IntoEnumIterator, IntoStaticStr};
-use ui::{List, prelude::*};
+use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
use ui_input::InputField;
use util::ResultExt;
@@ -1250,18 +1249,14 @@ impl Render for ConfigurationView {
.child(
List::new()
.child(
- InstructionListItem::new(
- "Grant permissions to the strategy you'll use according to the:",
- Some("Prerequisites"),
- Some("https://docs.aws.amazon.com/bedrock/latest/userguide/inference-prereq.html"),
- )
+ ListBulletItem::new("")
+ .child(Label::new("Grant permissions to the strategy you'll use according to the:"))
+ .child(ButtonLink::new("Prerequisites", "https://docs.aws.amazon.com/bedrock/latest/userguide/inference-prereq.html"))
)
.child(
- InstructionListItem::new(
- "Select the models you would like access to:",
- Some("Bedrock Model Catalog"),
- Some("https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/modelaccess"),
- )
+ ListBulletItem::new("")
+ .child(Label::new("Select the models you would like access to:"))
+ .child(ButtonLink::new("Bedrock Model Catalog", "https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/modelaccess"))
)
)
.child(self.render_static_credentials_ui())
@@ -1302,22 +1297,22 @@ impl ConfigurationView {
)
.child(
List::new()
- .child(InstructionListItem::new(
- "Create an IAM user in the AWS console with programmatic access",
- Some("IAM Console"),
- Some("https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/users"),
- ))
- .child(InstructionListItem::new(
- "Attach the necessary Bedrock permissions to this ",
- Some("user"),
- Some("https://docs.aws.amazon.com/bedrock/latest/userguide/inference-prereq.html"),
- ))
- .child(InstructionListItem::text_only(
- "Copy the access key ID and secret access key when provided",
- ))
- .child(InstructionListItem::text_only(
- "Enter these credentials below",
- )),
+ .child(
+ ListBulletItem::new("")
+ .child(Label::new("Create an IAM user in the AWS console with programmatic access"))
+ .child(ButtonLink::new("IAM Console", "https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/users"))
+ )
+ .child(
+ ListBulletItem::new("")
+ .child(Label::new("Attach the necessary Bedrock permissions to this"))
+ .child(ButtonLink::new("user", "https://docs.aws.amazon.com/bedrock/latest/userguide/inference-prereq.html"))
+ )
+ .child(
+ ListBulletItem::new("Copy the access key ID and secret access key when provided")
+ )
+ .child(
+ ListBulletItem::new("Enter these credentials below")
+ )
)
.child(self.access_key_id_editor.clone())
.child(self.secret_access_key_editor.clone())
@@ -14,7 +14,7 @@ use copilot::{Copilot, Status};
use futures::future::BoxFuture;
use futures::stream::BoxStream;
use futures::{FutureExt, Stream, StreamExt};
-use gpui::{Action, AnyView, App, AsyncApp, Entity, Render, Subscription, Task, svg};
+use gpui::{AnyView, App, AsyncApp, Entity, Subscription, Task};
use http_client::StatusCode;
use language::language_settings::all_language_settings;
use language_model::{
@@ -26,11 +26,9 @@ use language_model::{
StopReason, TokenUsage,
};
use settings::SettingsStore;
-use ui::{CommonAnimationExt, prelude::*};
+use ui::prelude::*;
use util::debug_panic;
-use crate::ui::ConfiguredApiCard;
-
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("copilot_chat");
const PROVIDER_NAME: LanguageModelProviderName =
LanguageModelProviderName::new("GitHub Copilot Chat");
@@ -179,8 +177,18 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
_: &mut Window,
cx: &mut App,
) -> AnyView {
- let state = self.state.clone();
- cx.new(|cx| ConfigurationView::new(state, cx)).into()
+ cx.new(|cx| {
+ copilot::ConfigurationView::new(
+ |cx| {
+ CopilotChat::global(cx)
+ .map(|m| m.read(cx).is_authenticated())
+ .unwrap_or(false)
+ },
+ copilot::ConfigurationMode::Chat,
+ cx,
+ )
+ })
+ .into()
}
fn reset_credentials(&self, _cx: &mut App) -> Task<Result<()>> {
@@ -1474,92 +1482,3 @@ mod tests {
);
}
}
-struct ConfigurationView {
- copilot_status: Option<copilot::Status>,
- state: Entity<State>,
- _subscription: Option<Subscription>,
-}
-
-impl ConfigurationView {
- pub fn new(state: Entity<State>, cx: &mut Context<Self>) -> Self {
- let copilot = Copilot::global(cx);
-
- Self {
- copilot_status: copilot.as_ref().map(|copilot| copilot.read(cx).status()),
- state,
- _subscription: copilot.as_ref().map(|copilot| {
- cx.observe(copilot, |this, model, cx| {
- this.copilot_status = Some(model.read(cx).status());
- cx.notify();
- })
- }),
- }
- }
-}
-
-impl Render for ConfigurationView {
- fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- if self.state.read(cx).is_authenticated(cx) {
- ConfiguredApiCard::new("Authorized")
- .button_label("Sign Out")
- .on_click(|_, window, cx| {
- window.dispatch_action(copilot::SignOut.boxed_clone(), cx);
- })
- .into_any_element()
- } else {
- let loading_icon = Icon::new(IconName::ArrowCircle).with_rotate_animation(4);
-
- const ERROR_LABEL: &str = "Copilot Chat requires an active GitHub Copilot subscription. Please ensure Copilot is configured and try again, or use a different Assistant provider.";
-
- match &self.copilot_status {
- Some(status) => match status {
- Status::Starting { task: _ } => h_flex()
- .gap_2()
- .child(loading_icon)
- .child(Label::new("Starting Copilotโฆ"))
- .into_any_element(),
- Status::SigningIn { prompt: _ }
- | Status::SignedOut {
- awaiting_signing_in: true,
- } => h_flex()
- .gap_2()
- .child(loading_icon)
- .child(Label::new("Signing into Copilotโฆ"))
- .into_any_element(),
- Status::Error(_) => {
- const LABEL: &str = "Copilot had issues starting. Please try restarting it. If the issue persists, try reinstalling Copilot.";
- v_flex()
- .gap_6()
- .child(Label::new(LABEL))
- .child(svg().size_8().path(IconName::CopilotError.path()))
- .into_any_element()
- }
- _ => {
- const LABEL: &str = "To use Zed's agent with GitHub Copilot, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot Chat subscription.";
-
- v_flex()
- .gap_2()
- .child(Label::new(LABEL))
- .child(
- Button::new("sign_in", "Sign in to use GitHub Copilot")
- .full_width()
- .style(ButtonStyle::Outlined)
- .icon_color(Color::Muted)
- .icon(IconName::Github)
- .icon_position(IconPosition::Start)
- .icon_size(IconSize::Small)
- .on_click(|_, window, cx| {
- copilot::initiate_sign_in(window, cx)
- }),
- )
- .into_any_element()
- }
- },
- None => v_flex()
- .gap_6()
- .child(Label::new(ERROR_LABEL))
- .into_any_element(),
- }
- }
- }
-}
@@ -7,11 +7,11 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture, stream::BoxStream
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
use http_client::HttpClient;
use language_model::{
- AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
- LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
- LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
- LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent,
- RateLimiter, Role, StopReason, TokenUsage,
+ ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
+ LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
+ LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
+ LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
+ LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, TokenUsage, env_var,
};
pub use settings::DeepseekAvailableModel as AvailableModel;
use settings::{Settings, SettingsStore};
@@ -19,13 +19,9 @@ use std::pin::Pin;
use std::str::FromStr;
use std::sync::{Arc, LazyLock};
-use ui::{List, prelude::*};
+use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
use ui_input::InputField;
use util::ResultExt;
-use zed_env_vars::{EnvVar, env_var};
-
-use crate::ui::ConfiguredApiCard;
-use crate::{api_key::ApiKeyState, ui::InstructionListItem};
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("deepseek");
const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("DeepSeek");
@@ -67,12 +63,8 @@ impl State {
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
let api_url = DeepSeekLanguageModelProvider::api_url(cx);
- self.api_key_state.load_if_needed(
- api_url,
- &API_KEY_ENV_VAR,
- |this| &mut this.api_key_state,
- cx,
- )
+ self.api_key_state
+ .load_if_needed(api_url, |this| &mut this.api_key_state, cx)
}
}
@@ -81,17 +73,13 @@ impl DeepSeekLanguageModelProvider {
let state = cx.new(|cx| {
cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
let api_url = Self::api_url(cx);
- this.api_key_state.handle_url_change(
- api_url,
- &API_KEY_ENV_VAR,
- |this| &mut this.api_key_state,
- cx,
- );
+ this.api_key_state
+ .handle_url_change(api_url, |this| &mut this.api_key_state, cx);
cx.notify();
})
.detach();
State {
- api_key_state: ApiKeyState::new(Self::api_url(cx)),
+ api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
}
});
@@ -632,12 +620,15 @@ impl Render for ConfigurationView {
.child(Label::new("To use DeepSeek in Zed, you need an API key:"))
.child(
List::new()
- .child(InstructionListItem::new(
- "Get your API key from the",
- Some("DeepSeek console"),
- Some("https://platform.deepseek.com/api_keys"),
- ))
- .child(InstructionListItem::text_only(
+ .child(
+ ListBulletItem::new("")
+ .child(Label::new("Get your API key from the"))
+ .child(ButtonLink::new(
+ "DeepSeek console",
+ "https://platform.deepseek.com/api_keys",
+ )),
+ )
+ .child(ListBulletItem::new(
"Paste your API key below and hit enter to start using the assistant",
)),
)
@@ -9,7 +9,7 @@ use google_ai::{
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
use http_client::HttpClient;
use language_model::{
- AuthenticateError, ConfigurationViewTargetAgent, LanguageModelCompletionError,
+ AuthenticateError, ConfigurationViewTargetAgent, EnvVar, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelToolChoice, LanguageModelToolSchemaFormat,
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, StopReason,
};
@@ -28,14 +28,11 @@ use std::sync::{
atomic::{self, AtomicU64},
};
use strum::IntoEnumIterator;
-use ui::{List, prelude::*};
+use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
use ui_input::InputField;
use util::ResultExt;
-use zed_env_vars::EnvVar;
-use crate::api_key::ApiKey;
-use crate::api_key::ApiKeyState;
-use crate::ui::{ConfiguredApiCard, InstructionListItem};
+use language_model::{ApiKey, ApiKeyState};
const PROVIDER_ID: LanguageModelProviderId = language_model::GOOGLE_PROVIDER_ID;
const PROVIDER_NAME: LanguageModelProviderName = language_model::GOOGLE_PROVIDER_NAME;
@@ -87,12 +84,8 @@ impl State {
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
let api_url = GoogleLanguageModelProvider::api_url(cx);
- self.api_key_state.load_if_needed(
- api_url,
- &API_KEY_ENV_VAR,
- |this| &mut this.api_key_state,
- cx,
- )
+ self.api_key_state
+ .load_if_needed(api_url, |this| &mut this.api_key_state, cx)
}
}
@@ -101,17 +94,13 @@ impl GoogleLanguageModelProvider {
let state = cx.new(|cx| {
cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
let api_url = Self::api_url(cx);
- this.api_key_state.handle_url_change(
- api_url,
- &API_KEY_ENV_VAR,
- |this| &mut this.api_key_state,
- cx,
- );
+ this.api_key_state
+ .handle_url_change(api_url, |this| &mut this.api_key_state, cx);
cx.notify();
})
.detach();
State {
- api_key_state: ApiKeyState::new(Self::api_url(cx)),
+ api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
}
});
@@ -873,14 +862,14 @@ impl Render for ConfigurationView {
})))
.child(
List::new()
- .child(InstructionListItem::new(
- "Create one by visiting",
- Some("Google AI's console"),
- Some("https://aistudio.google.com/app/apikey"),
- ))
- .child(InstructionListItem::text_only(
- "Paste your API key below and hit enter to start using the assistant",
- )),
+ .child(
+ ListBulletItem::new("")
+ .child(Label::new("Create one by visiting"))
+ .child(ButtonLink::new("Google AI's console", "https://aistudio.google.com/app/apikey"))
+ )
+ .child(
+ ListBulletItem::new("Paste your API key below and hit enter to start using the agent")
+ )
)
.child(self.api_key_editor.clone())
.child(
@@ -20,11 +20,10 @@ use settings::{Settings, SettingsStore};
use std::pin::Pin;
use std::str::FromStr;
use std::{collections::BTreeMap, sync::Arc};
-use ui::{ButtonLike, Indicator, List, prelude::*};
+use ui::{ButtonLike, Indicator, InlineCode, List, ListBulletItem, prelude::*};
use util::ResultExt;
use crate::AllLanguageModelSettings;
-use crate::ui::InstructionListItem;
const LMSTUDIO_DOWNLOAD_URL: &str = "https://lmstudio.ai/download";
const LMSTUDIO_CATALOG_URL: &str = "https://lmstudio.ai/models";
@@ -686,12 +685,14 @@ impl Render for ConfigurationView {
.child(
v_flex().gap_1().child(Label::new(lmstudio_intro)).child(
List::new()
- .child(InstructionListItem::text_only(
+ .child(ListBulletItem::new(
"LM Studio needs to be running with at least one model downloaded.",
))
- .child(InstructionListItem::text_only(
- "To get your first model, try running `lms get qwen2.5-coder-7b`",
- )),
+ .child(
+ ListBulletItem::new("")
+ .child(Label::new("To get your first model, try running"))
+ .child(InlineCode::new("lms get qwen2.5-coder-7b")),
+ ),
),
)
.child(
@@ -1,31 +1,27 @@
use anyhow::{Result, anyhow};
use collections::BTreeMap;
-use fs::Fs;
+
use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture, stream::BoxStream};
use gpui::{AnyView, App, AsyncApp, Context, Entity, Global, SharedString, Task, Window};
use http_client::HttpClient;
use language_model::{
- AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
- LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
- LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
- LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent,
- RateLimiter, Role, StopReason, TokenUsage,
+ ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
+ LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
+ LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
+ LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
+ LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, TokenUsage, env_var,
};
-use mistral::{CODESTRAL_API_URL, MISTRAL_API_URL, StreamResponse};
+pub use mistral::{CODESTRAL_API_URL, MISTRAL_API_URL, StreamResponse};
pub use settings::MistralAvailableModel as AvailableModel;
-use settings::{EditPredictionProvider, Settings, SettingsStore, update_settings_file};
+use settings::{Settings, SettingsStore};
use std::collections::HashMap;
use std::pin::Pin;
use std::str::FromStr;
-use std::sync::{Arc, LazyLock};
+use std::sync::{Arc, LazyLock, OnceLock};
use strum::IntoEnumIterator;
-use ui::{List, prelude::*};
+use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
use ui_input::InputField;
use util::ResultExt;
-use zed_env_vars::{EnvVar, env_var};
-
-use crate::ui::ConfiguredApiCard;
-use crate::{api_key::ApiKeyState, ui::InstructionListItem};
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("mistral");
const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Mistral");
@@ -35,6 +31,7 @@ static API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(API_KEY_ENV_VAR_NAME);
const CODESTRAL_API_KEY_ENV_VAR_NAME: &str = "CODESTRAL_API_KEY";
static CODESTRAL_API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(CODESTRAL_API_KEY_ENV_VAR_NAME);
+static CODESTRAL_API_KEY: OnceLock<Entity<ApiKeyState>> = OnceLock::new();
#[derive(Default, Clone, Debug, PartialEq)]
pub struct MistralSettings {
@@ -44,12 +41,22 @@ pub struct MistralSettings {
pub struct MistralLanguageModelProvider {
http_client: Arc<dyn HttpClient>,
- state: Entity<State>,
+ pub state: Entity<State>,
}
pub struct State {
api_key_state: ApiKeyState,
- codestral_api_key_state: ApiKeyState,
+ codestral_api_key_state: Entity<ApiKeyState>,
+}
+
+pub fn codestral_api_key(cx: &mut App) -> Entity<ApiKeyState> {
+ return CODESTRAL_API_KEY
+ .get_or_init(|| {
+ cx.new(|_| {
+ ApiKeyState::new(CODESTRAL_API_URL.into(), CODESTRAL_API_KEY_ENV_VAR.clone())
+ })
+ })
+ .clone();
}
impl State {
@@ -63,39 +70,19 @@ impl State {
.store(api_url, api_key, |this| &mut this.api_key_state, cx)
}
- fn set_codestral_api_key(
- &mut self,
- api_key: Option<String>,
- cx: &mut Context<Self>,
- ) -> Task<Result<()>> {
- self.codestral_api_key_state.store(
- CODESTRAL_API_URL.into(),
- api_key,
- |this| &mut this.codestral_api_key_state,
- cx,
- )
- }
-
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
let api_url = MistralLanguageModelProvider::api_url(cx);
- self.api_key_state.load_if_needed(
- api_url,
- &API_KEY_ENV_VAR,
- |this| &mut this.api_key_state,
- cx,
- )
+ self.api_key_state
+ .load_if_needed(api_url, |this| &mut this.api_key_state, cx)
}
fn authenticate_codestral(
&mut self,
cx: &mut Context<Self>,
) -> Task<Result<(), AuthenticateError>> {
- self.codestral_api_key_state.load_if_needed(
- CODESTRAL_API_URL.into(),
- &CODESTRAL_API_KEY_ENV_VAR,
- |this| &mut this.codestral_api_key_state,
- cx,
- )
+ self.codestral_api_key_state.update(cx, |state, cx| {
+ state.load_if_needed(CODESTRAL_API_URL.into(), |state| state, cx)
+ })
}
}
@@ -116,18 +103,14 @@ impl MistralLanguageModelProvider {
let state = cx.new(|cx| {
cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
let api_url = Self::api_url(cx);
- this.api_key_state.handle_url_change(
- api_url,
- &API_KEY_ENV_VAR,
- |this| &mut this.api_key_state,
- cx,
- );
+ this.api_key_state
+ .handle_url_change(api_url, |this| &mut this.api_key_state, cx);
cx.notify();
})
.detach();
State {
- api_key_state: ApiKeyState::new(Self::api_url(cx)),
- codestral_api_key_state: ApiKeyState::new(CODESTRAL_API_URL.into()),
+ api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
+ codestral_api_key_state: codestral_api_key(cx),
}
});
@@ -142,7 +125,11 @@ impl MistralLanguageModelProvider {
}
pub fn codestral_api_key(&self, url: &str, cx: &App) -> Option<Arc<str>> {
- self.state.read(cx).codestral_api_key_state.key(url)
+ self.state
+ .read(cx)
+ .codestral_api_key_state
+ .read(cx)
+ .key(url)
}
fn create_language_model(&self, model: mistral::Model) -> Arc<dyn LanguageModel> {
@@ -159,7 +146,7 @@ impl MistralLanguageModelProvider {
&crate::AllLanguageModelSettings::get_global(cx).mistral
}
- fn api_url(cx: &App) -> SharedString {
+ pub fn api_url(cx: &App) -> SharedString {
let api_url = &Self::settings(cx).api_url;
if api_url.is_empty() {
mistral::MISTRAL_API_URL.into()
@@ -747,7 +734,6 @@ struct RawToolCall {
struct ConfigurationView {
api_key_editor: Entity<InputField>,
- codestral_api_key_editor: Entity<InputField>,
state: Entity<State>,
load_credentials_task: Option<Task<()>>,
}
@@ -756,8 +742,6 @@ impl ConfigurationView {
fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let api_key_editor =
cx.new(|cx| InputField::new(window, cx, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
- let codestral_api_key_editor =
- cx.new(|cx| InputField::new(window, cx, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
cx.observe(&state, |_, _, cx| {
cx.notify();
@@ -774,12 +758,6 @@ impl ConfigurationView {
// We don't log an error, because "not signed in" is also an error.
let _ = task.await;
}
- if let Some(task) = state
- .update(cx, |state, cx| state.authenticate_codestral(cx))
- .log_err()
- {
- let _ = task.await;
- }
this.update(cx, |this, cx| {
this.load_credentials_task = None;
@@ -791,7 +769,6 @@ impl ConfigurationView {
Self {
api_key_editor,
- codestral_api_key_editor,
state,
load_credentials_task,
}
@@ -829,110 +806,9 @@ impl ConfigurationView {
.detach_and_log_err(cx);
}
- fn save_codestral_api_key(
- &mut self,
- _: &menu::Confirm,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- let api_key = self
- .codestral_api_key_editor
- .read(cx)
- .text(cx)
- .trim()
- .to_string();
- if api_key.is_empty() {
- return;
- }
-
- // url changes can cause the editor to be displayed again
- self.codestral_api_key_editor
- .update(cx, |editor, cx| editor.set_text("", window, cx));
-
- let state = self.state.clone();
- cx.spawn_in(window, async move |_, cx| {
- state
- .update(cx, |state, cx| {
- state.set_codestral_api_key(Some(api_key), cx)
- })?
- .await?;
- cx.update(|_window, cx| {
- set_edit_prediction_provider(EditPredictionProvider::Codestral, cx)
- })
- })
- .detach_and_log_err(cx);
- }
-
- fn reset_codestral_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- self.codestral_api_key_editor
- .update(cx, |editor, cx| editor.set_text("", window, cx));
-
- let state = self.state.clone();
- cx.spawn_in(window, async move |_, cx| {
- state
- .update(cx, |state, cx| state.set_codestral_api_key(None, cx))?
- .await?;
- cx.update(|_window, cx| set_edit_prediction_provider(EditPredictionProvider::Zed, cx))
- })
- .detach_and_log_err(cx);
- }
-
fn should_render_api_key_editor(&self, cx: &mut Context<Self>) -> bool {
!self.state.read(cx).is_authenticated()
}
-
- fn render_codestral_api_key_editor(&mut self, cx: &mut Context<Self>) -> AnyElement {
- let key_state = &self.state.read(cx).codestral_api_key_state;
- let should_show_editor = !key_state.has_key();
- let env_var_set = key_state.is_from_env_var();
- let configured_card_label = if env_var_set {
- format!("API key set in {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable")
- } else {
- "Codestral API key configured".to_string()
- };
-
- if should_show_editor {
- v_flex()
- .id("codestral")
- .size_full()
- .mt_2()
- .on_action(cx.listener(Self::save_codestral_api_key))
- .child(Label::new(
- "To use Codestral as an edit prediction provider, \
- you need to add a Codestral-specific API key. Follow these steps:",
- ))
- .child(
- List::new()
- .child(InstructionListItem::new(
- "Create one by visiting",
- Some("the Codestral section of Mistral's console"),
- Some("https://console.mistral.ai/codestral"),
- ))
- .child(InstructionListItem::text_only("Paste your API key below and hit enter")),
- )
- .child(self.codestral_api_key_editor.clone())
- .child(
- Label::new(
- format!("You can also assign the {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable and restart Zed."),
- )
- .size(LabelSize::Small).color(Color::Muted),
- ).into_any()
- } else {
- ConfiguredApiCard::new(configured_card_label)
- .disabled(env_var_set)
- .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)))
- .when(env_var_set, |this| {
- this.tooltip_label(format!(
- "To reset your API key, \
- unset the {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable."
- ))
- })
- .on_click(
- cx.listener(|this, _, window, cx| this.reset_codestral_api_key(window, cx)),
- )
- .into_any_element()
- }
- }
}
impl Render for ConfigurationView {
@@ -958,17 +834,17 @@ impl Render for ConfigurationView {
.child(Label::new("To use Zed's agent with Mistral, you need to add an API key. Follow these steps:"))
.child(
List::new()
- .child(InstructionListItem::new(
- "Create one by visiting",
- Some("Mistral's console"),
- Some("https://console.mistral.ai/api-keys"),
- ))
- .child(InstructionListItem::text_only(
- "Ensure your Mistral account has credits",
- ))
- .child(InstructionListItem::text_only(
- "Paste your API key below and hit enter to start using the assistant",
- )),
+ .child(
+ ListBulletItem::new("")
+ .child(Label::new("Create one by visiting"))
+ .child(ButtonLink::new("Mistral's console", "https://console.mistral.ai/api-keys"))
+ )
+ .child(
+ ListBulletItem::new("Ensure your Mistral account has credits")
+ )
+ .child(
+ ListBulletItem::new("Paste your API key below and hit enter to start using the assistant")
+ ),
)
.child(self.api_key_editor.clone())
.child(
@@ -977,7 +853,6 @@ impl Render for ConfigurationView {
)
.size(LabelSize::Small).color(Color::Muted),
)
- .child(self.render_codestral_api_key_editor(cx))
.into_any()
} else {
v_flex()
@@ -994,24 +869,11 @@ impl Render for ConfigurationView {
))
}),
)
- .child(self.render_codestral_api_key_editor(cx))
.into_any()
}
}
}
-fn set_edit_prediction_provider(provider: EditPredictionProvider, cx: &mut App) {
- let fs = <dyn Fs>::global(cx);
- update_settings_file(fs, cx, move |settings, _| {
- settings
- .project
- .all_languages
- .features
- .get_or_insert_default()
- .edit_prediction_provider = Some(provider);
- });
-}
-
#[cfg(test)]
mod tests {
use super::*;
@@ -5,11 +5,11 @@ use futures::{Stream, TryFutureExt, stream};
use gpui::{AnyView, App, AsyncApp, Context, CursorStyle, Entity, Task};
use http_client::HttpClient;
use language_model::{
- AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
- LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
- LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
- LanguageModelRequestTool, LanguageModelToolChoice, LanguageModelToolUse,
- LanguageModelToolUseId, MessageContent, RateLimiter, Role, StopReason, TokenUsage,
+ ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
+ LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
+ LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
+ LanguageModelRequest, LanguageModelRequestTool, LanguageModelToolChoice, LanguageModelToolUse,
+ LanguageModelToolUseId, MessageContent, RateLimiter, Role, StopReason, TokenUsage, env_var,
};
use menu;
use ollama::{
@@ -22,13 +22,13 @@ use std::pin::Pin;
use std::sync::LazyLock;
use std::sync::atomic::{AtomicU64, Ordering};
use std::{collections::HashMap, sync::Arc};
-use ui::{ButtonLike, ElevationIndex, List, Tooltip, prelude::*};
+use ui::{
+ ButtonLike, ButtonLink, ConfiguredApiCard, ElevationIndex, InlineCode, List, ListBulletItem,
+ Tooltip, prelude::*,
+};
use ui_input::InputField;
-use zed_env_vars::{EnvVar, env_var};
use crate::AllLanguageModelSettings;
-use crate::api_key::ApiKeyState;
-use crate::ui::{ConfiguredApiCard, InstructionListItem};
const OLLAMA_DOWNLOAD_URL: &str = "https://ollama.com/download";
const OLLAMA_LIBRARY_URL: &str = "https://ollama.com/library";
@@ -80,12 +80,9 @@ impl State {
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
let api_url = OllamaLanguageModelProvider::api_url(cx);
- let task = self.api_key_state.load_if_needed(
- api_url,
- &API_KEY_ENV_VAR,
- |this| &mut this.api_key_state,
- cx,
- );
+ let task = self
+ .api_key_state
+ .load_if_needed(api_url, |this| &mut this.api_key_state, cx);
// Always try to fetch models - if no API key is needed (local Ollama), it will work
// If API key is needed and provided, it will work
@@ -185,7 +182,7 @@ impl OllamaLanguageModelProvider {
http_client,
fetched_models: Default::default(),
fetch_model_task: None,
- api_key_state: ApiKeyState::new(Self::api_url(cx)),
+ api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
}
}),
};
@@ -733,15 +730,17 @@ impl ConfigurationView {
.child(Label::new("To use local Ollama:"))
.child(
List::new()
- .child(InstructionListItem::new(
- "Download and install Ollama from",
- Some("ollama.com"),
- Some("https://ollama.com/download"),
- ))
- .child(InstructionListItem::text_only(
- "Start Ollama and download a model: `ollama run gpt-oss:20b`",
- ))
- .child(InstructionListItem::text_only(
+ .child(
+ ListBulletItem::new("")
+ .child(Label::new("Download and install Ollama from"))
+ .child(ButtonLink::new("ollama.com", "https://ollama.com/download")),
+ )
+ .child(
+ ListBulletItem::new("")
+ .child(Label::new("Start Ollama and download a model:"))
+ .child(InlineCode::new("ollama run gpt-oss:20b")),
+ )
+ .child(ListBulletItem::new(
"Click 'Connect' below to start using Ollama in Zed",
)),
)
@@ -5,11 +5,11 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
use http_client::HttpClient;
use language_model::{
- AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
- LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
- LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
- LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent,
- RateLimiter, Role, StopReason, TokenUsage,
+ ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
+ LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
+ LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
+ LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
+ LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, TokenUsage, env_var,
};
use menu;
use open_ai::{
@@ -20,13 +20,9 @@ use std::pin::Pin;
use std::str::FromStr as _;
use std::sync::{Arc, LazyLock};
use strum::IntoEnumIterator;
-use ui::{List, prelude::*};
+use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
use ui_input::InputField;
use util::ResultExt;
-use zed_env_vars::{EnvVar, env_var};
-
-use crate::ui::ConfiguredApiCard;
-use crate::{api_key::ApiKeyState, ui::InstructionListItem};
const PROVIDER_ID: LanguageModelProviderId = language_model::OPEN_AI_PROVIDER_ID;
const PROVIDER_NAME: LanguageModelProviderName = language_model::OPEN_AI_PROVIDER_NAME;
@@ -62,12 +58,8 @@ impl State {
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
let api_url = OpenAiLanguageModelProvider::api_url(cx);
- self.api_key_state.load_if_needed(
- api_url,
- &API_KEY_ENV_VAR,
- |this| &mut this.api_key_state,
- cx,
- )
+ self.api_key_state
+ .load_if_needed(api_url, |this| &mut this.api_key_state, cx)
}
}
@@ -76,17 +68,13 @@ impl OpenAiLanguageModelProvider {
let state = cx.new(|cx| {
cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
let api_url = Self::api_url(cx);
- this.api_key_state.handle_url_change(
- api_url,
- &API_KEY_ENV_VAR,
- |this| &mut this.api_key_state,
- cx,
- );
+ this.api_key_state
+ .handle_url_change(api_url, |this| &mut this.api_key_state, cx);
cx.notify();
})
.detach();
State {
- api_key_state: ApiKeyState::new(Self::api_url(cx)),
+ api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
}
});
@@ -790,17 +778,17 @@ impl Render for ConfigurationView {
.child(Label::new("To use Zed's agent with OpenAI, you need to add an API key. Follow these steps:"))
.child(
List::new()
- .child(InstructionListItem::new(
- "Create one by visiting",
- Some("OpenAI's console"),
- Some("https://platform.openai.com/api-keys"),
- ))
- .child(InstructionListItem::text_only(
- "Ensure your OpenAI account has credits",
- ))
- .child(InstructionListItem::text_only(
- "Paste your API key below and hit enter to start using the assistant",
- )),
+ .child(
+ ListBulletItem::new("")
+ .child(Label::new("Create one by visiting"))
+ .child(ButtonLink::new("OpenAI's console", "https://platform.openai.com/api-keys"))
+ )
+ .child(
+ ListBulletItem::new("Ensure your OpenAI account has credits")
+ )
+ .child(
+ ListBulletItem::new("Paste your API key below and hit enter to start using the agent")
+ ),
)
.child(self.api_key_editor.clone())
.child(
@@ -4,10 +4,10 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
use http_client::HttpClient;
use language_model::{
- AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
- LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
- LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
- LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter,
+ ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
+ LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
+ LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
+ LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter,
};
use menu;
use open_ai::{ResponseStreamEvent, stream_completion};
@@ -16,9 +16,7 @@ use std::sync::Arc;
use ui::{ElevationIndex, Tooltip, prelude::*};
use ui_input::InputField;
use util::ResultExt;
-use zed_env_vars::EnvVar;
-use crate::api_key::ApiKeyState;
use crate::provider::open_ai::{OpenAiEventMapper, into_open_ai};
pub use settings::OpenAiCompatibleAvailableModel as AvailableModel;
pub use settings::OpenAiCompatibleModelCapabilities as ModelCapabilities;
@@ -38,7 +36,6 @@ pub struct OpenAiCompatibleLanguageModelProvider {
pub struct State {
id: Arc<str>,
- api_key_env_var: EnvVar,
api_key_state: ApiKeyState,
settings: OpenAiCompatibleSettings,
}
@@ -56,12 +53,8 @@ impl State {
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
let api_url = SharedString::new(self.settings.api_url.clone());
- self.api_key_state.load_if_needed(
- api_url,
- &self.api_key_env_var,
- |this| &mut this.api_key_state,
- cx,
- )
+ self.api_key_state
+ .load_if_needed(api_url, |this| &mut this.api_key_state, cx)
}
}
@@ -83,7 +76,6 @@ impl OpenAiCompatibleLanguageModelProvider {
let api_url = SharedString::new(settings.api_url.as_str());
this.api_key_state.handle_url_change(
api_url,
- &this.api_key_env_var,
|this| &mut this.api_key_state,
cx,
);
@@ -95,8 +87,10 @@ impl OpenAiCompatibleLanguageModelProvider {
let settings = resolve_settings(&id, cx).cloned().unwrap_or_default();
State {
id: id.clone(),
- api_key_env_var: EnvVar::new(api_key_env_var_name),
- api_key_state: ApiKeyState::new(SharedString::new(settings.api_url.as_str())),
+ api_key_state: ApiKeyState::new(
+ SharedString::new(settings.api_url.as_str()),
+ EnvVar::new(api_key_env_var_name),
+ ),
settings,
}
});
@@ -437,7 +431,7 @@ impl Render for ConfigurationView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let state = self.state.read(cx);
let env_var_set = state.api_key_state.is_from_env_var();
- let env_var_name = &state.api_key_env_var.name;
+ let env_var_name = state.api_key_state.env_var_name();
let api_key_section = if self.should_render_editor(cx) {
v_flex()
@@ -4,11 +4,12 @@ use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task};
use http_client::HttpClient;
use language_model::{
- AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
- LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
- LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
- LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolSchemaFormat,
- LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, TokenUsage,
+ ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
+ LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
+ LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
+ LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
+ LanguageModelToolSchemaFormat, LanguageModelToolUse, MessageContent, RateLimiter, Role,
+ StopReason, TokenUsage, env_var,
};
use open_router::{
Model, ModelMode as OpenRouterModelMode, OPEN_ROUTER_API_URL, ResponseStreamEvent, list_models,
@@ -17,13 +18,9 @@ use settings::{OpenRouterAvailableModel as AvailableModel, Settings, SettingsSto
use std::pin::Pin;
use std::str::FromStr as _;
use std::sync::{Arc, LazyLock};
-use ui::{List, prelude::*};
+use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
use ui_input::InputField;
use util::ResultExt;
-use zed_env_vars::{EnvVar, env_var};
-
-use crate::ui::ConfiguredApiCard;
-use crate::{api_key::ApiKeyState, ui::InstructionListItem};
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("openrouter");
const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("OpenRouter");
@@ -62,12 +59,9 @@ impl State {
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
let api_url = OpenRouterLanguageModelProvider::api_url(cx);
- let task = self.api_key_state.load_if_needed(
- api_url,
- &API_KEY_ENV_VAR,
- |this| &mut this.api_key_state,
- cx,
- );
+ let task = self
+ .api_key_state
+ .load_if_needed(api_url, |this| &mut this.api_key_state, cx);
cx.spawn(async move |this, cx| {
let result = task.await;
@@ -135,7 +129,7 @@ impl OpenRouterLanguageModelProvider {
})
.detach();
State {
- api_key_state: ApiKeyState::new(Self::api_url(cx)),
+ api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
http_client: http_client.clone(),
available_models: Vec::new(),
fetch_models_task: None,
@@ -830,17 +824,15 @@ impl Render for ConfigurationView {
.child(Label::new("To use Zed's agent with OpenRouter, you need to add an API key. Follow these steps:"))
.child(
List::new()
- .child(InstructionListItem::new(
- "Create an API key by visiting",
- Some("OpenRouter's console"),
- Some("https://openrouter.ai/keys"),
- ))
- .child(InstructionListItem::text_only(
- "Ensure your OpenRouter account has credits",
- ))
- .child(InstructionListItem::text_only(
- "Paste your API key below and hit enter to start using the assistant",
- )),
+ .child(
+ ListBulletItem::new("")
+ .child(Label::new("Create an API key by visiting"))
+ .child(ButtonLink::new("OpenRouter's console", "https://openrouter.ai/keys"))
+ )
+ .child(ListBulletItem::new("Ensure your OpenRouter account has credits")
+ )
+ .child(ListBulletItem::new("Paste your API key below and hit enter to start using the assistant")
+ ),
)
.child(self.api_key_editor.clone())
.child(
@@ -4,26 +4,20 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
use http_client::HttpClient;
use language_model::{
- AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
- LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
- LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
- LanguageModelToolChoice, RateLimiter, Role,
+ ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
+ LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
+ LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
+ LanguageModelRequest, LanguageModelToolChoice, RateLimiter, Role, env_var,
};
use open_ai::ResponseStreamEvent;
pub use settings::VercelAvailableModel as AvailableModel;
use settings::{Settings, SettingsStore};
use std::sync::{Arc, LazyLock};
use strum::IntoEnumIterator;
-use ui::{List, prelude::*};
+use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
use ui_input::InputField;
use util::ResultExt;
use vercel::{Model, VERCEL_API_URL};
-use zed_env_vars::{EnvVar, env_var};
-
-use crate::{
- api_key::ApiKeyState,
- ui::{ConfiguredApiCard, InstructionListItem},
-};
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("vercel");
const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Vercel");
@@ -59,12 +53,8 @@ impl State {
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
let api_url = VercelLanguageModelProvider::api_url(cx);
- self.api_key_state.load_if_needed(
- api_url,
- &API_KEY_ENV_VAR,
- |this| &mut this.api_key_state,
- cx,
- )
+ self.api_key_state
+ .load_if_needed(api_url, |this| &mut this.api_key_state, cx)
}
}
@@ -73,17 +63,13 @@ impl VercelLanguageModelProvider {
let state = cx.new(|cx| {
cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
let api_url = Self::api_url(cx);
- this.api_key_state.handle_url_change(
- api_url,
- &API_KEY_ENV_VAR,
- |this| &mut this.api_key_state,
- cx,
- );
+ this.api_key_state
+ .handle_url_change(api_url, |this| &mut this.api_key_state, cx);
cx.notify();
})
.detach();
State {
- api_key_state: ApiKeyState::new(Self::api_url(cx)),
+ api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
}
});
@@ -472,14 +458,14 @@ impl Render for ConfigurationView {
.child(Label::new("To use Zed's agent with Vercel v0, you need to add an API key. Follow these steps:"))
.child(
List::new()
- .child(InstructionListItem::new(
- "Create one by visiting",
- Some("Vercel v0's console"),
- Some("https://v0.dev/chat/settings/keys"),
- ))
- .child(InstructionListItem::text_only(
- "Paste your API key below and hit enter to start using the agent",
- )),
+ .child(
+ ListBulletItem::new("")
+ .child(Label::new("Create one by visiting"))
+ .child(ButtonLink::new("Vercel v0's console", "https://v0.dev/chat/settings/keys"))
+ )
+ .child(
+ ListBulletItem::new("Paste your API key below and hit enter to start using the agent")
+ ),
)
.child(self.api_key_editor.clone())
.child(
@@ -4,26 +4,21 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, Task, Window};
use http_client::HttpClient;
use language_model::{
- AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
- LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
- LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
- LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter, Role,
+ ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
+ LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
+ LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
+ LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter,
+ Role, env_var,
};
use open_ai::ResponseStreamEvent;
pub use settings::XaiAvailableModel as AvailableModel;
use settings::{Settings, SettingsStore};
use std::sync::{Arc, LazyLock};
use strum::IntoEnumIterator;
-use ui::{List, prelude::*};
+use ui::{ButtonLink, ConfiguredApiCard, List, ListBulletItem, prelude::*};
use ui_input::InputField;
use util::ResultExt;
use x_ai::{Model, XAI_API_URL};
-use zed_env_vars::{EnvVar, env_var};
-
-use crate::{
- api_key::ApiKeyState,
- ui::{ConfiguredApiCard, InstructionListItem},
-};
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("x_ai");
const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("xAI");
@@ -59,12 +54,8 @@ impl State {
fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
let api_url = XAiLanguageModelProvider::api_url(cx);
- self.api_key_state.load_if_needed(
- api_url,
- &API_KEY_ENV_VAR,
- |this| &mut this.api_key_state,
- cx,
- )
+ self.api_key_state
+ .load_if_needed(api_url, |this| &mut this.api_key_state, cx)
}
}
@@ -73,17 +64,13 @@ impl XAiLanguageModelProvider {
let state = cx.new(|cx| {
cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
let api_url = Self::api_url(cx);
- this.api_key_state.handle_url_change(
- api_url,
- &API_KEY_ENV_VAR,
- |this| &mut this.api_key_state,
- cx,
- );
+ this.api_key_state
+ .handle_url_change(api_url, |this| &mut this.api_key_state, cx);
cx.notify();
})
.detach();
State {
- api_key_state: ApiKeyState::new(Self::api_url(cx)),
+ api_key_state: ApiKeyState::new(Self::api_url(cx), (*API_KEY_ENV_VAR).clone()),
}
});
@@ -474,14 +461,14 @@ impl Render for ConfigurationView {
.child(Label::new("To use Zed's agent with xAI, you need to add an API key. Follow these steps:"))
.child(
List::new()
- .child(InstructionListItem::new(
- "Create one by visiting",
- Some("xAI console"),
- Some("https://console.x.ai/team/default/api-keys"),
- ))
- .child(InstructionListItem::text_only(
- "Paste your API key below and hit enter to start using the agent",
- )),
+ .child(
+ ListBulletItem::new("")
+ .child(Label::new("Create one by visiting"))
+ .child(ButtonLink::new("xAI console", "https://console.x.ai/team/default/api-keys"))
+ )
+ .child(
+ ListBulletItem::new("Paste your API key below and hit enter to start using the agent")
+ ),
)
.child(self.api_key_editor.clone())
.child(
@@ -1,4 +0,0 @@
-pub mod configured_api_card;
-pub mod instruction_list_item;
-pub use configured_api_card::ConfiguredApiCard;
-pub use instruction_list_item::InstructionListItem;
@@ -1,69 +0,0 @@
-use gpui::{AnyElement, IntoElement, ParentElement, SharedString};
-use ui::{ListItem, prelude::*};
-
-/// A reusable list item component for adding LLM provider configuration instructions
-pub struct InstructionListItem {
- label: SharedString,
- button_label: Option<SharedString>,
- button_link: Option<String>,
-}
-
-impl InstructionListItem {
- pub fn new(
- label: impl Into<SharedString>,
- button_label: Option<impl Into<SharedString>>,
- button_link: Option<impl Into<String>>,
- ) -> Self {
- Self {
- label: label.into(),
- button_label: button_label.map(|l| l.into()),
- button_link: button_link.map(|l| l.into()),
- }
- }
-
- pub fn text_only(label: impl Into<SharedString>) -> Self {
- Self {
- label: label.into(),
- button_label: None,
- button_link: None,
- }
- }
-}
-
-impl IntoElement for InstructionListItem {
- type Element = AnyElement;
-
- fn into_element(self) -> Self::Element {
- let item_content = if let (Some(button_label), Some(button_link)) =
- (self.button_label, self.button_link)
- {
- let link = button_link;
- let unique_id = SharedString::from(format!("{}-button", self.label));
-
- h_flex()
- .flex_wrap()
- .child(Label::new(self.label))
- .child(
- Button::new(unique_id, button_label)
- .style(ButtonStyle::Subtle)
- .icon(IconName::ArrowUpRight)
- .icon_size(IconSize::Small)
- .icon_color(Color::Muted)
- .on_click(move |_, _window, cx| cx.open_url(&link)),
- )
- .into_any_element()
- } else {
- Label::new(self.label).into_any_element()
- };
-
- ListItem::new("list-item")
- .selectable(false)
- .start_slot(
- Icon::new(IconName::Dash)
- .size(IconSize::XSmall)
- .color(Color::Hidden),
- )
- .child(div().w_full().child(item_content))
- .into_any_element()
- }
-}
@@ -186,22 +186,20 @@ pub struct CopilotSettingsContent {
pub enterprise_uri: Option<String>,
}
+#[with_fallible_options]
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
pub struct CodestralSettingsContent {
/// Model to use for completions.
///
/// Default: "codestral-latest"
- #[serde(default)]
pub model: Option<String>,
/// Maximum tokens to generate.
///
/// Default: 150
- #[serde(default)]
pub max_tokens: Option<u32>,
/// Api URL to use for completions.
///
/// Default: "https://codestral.mistral.ai"
- #[serde(default)]
pub api_url: Option<String>,
}
@@ -18,6 +18,9 @@ test-support = []
[dependencies]
anyhow.workspace = true
bm25 = "2.3.2"
+copilot.workspace = true
+edit_prediction.workspace = true
+language_models.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
@@ -38,8 +41,8 @@ strum.workspace = true
telemetry.workspace = true
theme.workspace = true
title_bar.workspace = true
-ui.workspace = true
ui_input.workspace = true
+ui.workspace = true
util.workspace = true
workspace.workspace = true
zed_actions.workspace = true
@@ -2,10 +2,12 @@ mod dropdown;
mod font_picker;
mod icon_theme_picker;
mod input_field;
+mod section_items;
mod theme_picker;
pub use dropdown::*;
pub use font_picker::font_picker;
pub use icon_theme_picker::icon_theme_picker;
pub use input_field::*;
+pub use section_items::*;
pub use theme_picker::theme_picker;
@@ -13,6 +13,7 @@ pub struct SettingsInputField {
tab_index: Option<isize>,
}
+// TODO: Update the `ui_input::InputField` to use `window.use_state` and `RenceOnce` and remove this component
impl SettingsInputField {
pub fn new() -> Self {
Self {
@@ -0,0 +1,56 @@
+use gpui::{IntoElement, ParentElement, Styled};
+use ui::{Divider, DividerColor, prelude::*};
+
+#[derive(IntoElement)]
+pub struct SettingsSectionHeader {
+ icon: Option<IconName>,
+ label: SharedString,
+ no_padding: bool,
+}
+
+impl SettingsSectionHeader {
+ pub fn new(label: impl Into<SharedString>) -> Self {
+ Self {
+ label: label.into(),
+ icon: None,
+ no_padding: false,
+ }
+ }
+
+ pub fn icon(mut self, icon: IconName) -> Self {
+ self.icon = Some(icon);
+ self
+ }
+
+ pub fn no_padding(mut self, no_padding: bool) -> Self {
+ self.no_padding = no_padding;
+ self
+ }
+}
+
+impl RenderOnce for SettingsSectionHeader {
+ fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
+ let label = Label::new(self.label)
+ .size(LabelSize::Small)
+ .color(Color::Muted)
+ .buffer_font(cx);
+
+ v_flex()
+ .w_full()
+ .when(!self.no_padding, |this| this.px_8())
+ .gap_1p5()
+ .map(|this| {
+ if self.icon.is_some() {
+ this.child(
+ h_flex()
+ .gap_1p5()
+ .child(Icon::new(self.icon.unwrap()).color(Color::Muted))
+ .child(label),
+ )
+ } else {
+ this.child(label)
+ }
+ })
+ .child(Divider::horizontal().color(DividerColor::BorderFaded))
+ }
+}
@@ -2330,8 +2330,12 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
// Note that `crates/json_schema_store` solves the same problem, there is probably a way to unify the two
items.push(SettingsPageItem::SectionHeader(LANGUAGES_SECTION_HEADER));
items.extend(all_language_names(cx).into_iter().map(|language_name| {
+ let link = format!("languages.{language_name}");
SettingsPageItem::SubPageLink(SubPageLink {
title: language_name,
+ description: None,
+ json_path: Some(link.leak()),
+ in_json: true,
files: USER | PROJECT,
render: Arc::new(|this, window, cx| {
this.render_sub_page_items(
@@ -6013,7 +6017,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
files: USER,
}),
SettingsPageItem::SettingItem(SettingItem {
- title: "In Text Threads",
+ title: "Display In Text Threads",
description: "Whether edit predictions are enabled when editing text threads in the agent panel.",
field: Box::new(SettingField {
json_path: Some("edit_prediction.in_text_threads"),
@@ -6027,42 +6031,6 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
metadata: None,
files: USER,
}),
- SettingsPageItem::SettingItem(SettingItem {
- title: "Copilot Provider",
- description: "Use GitHub Copilot as your edit prediction provider.",
- field: Box::new(
- SettingField {
- json_path: Some("edit_prediction.copilot_provider"),
- pick: |settings_content| {
- settings_content.project.all_languages.edit_predictions.as_ref()?.copilot.as_ref()
- },
- write: |settings_content, value| {
- settings_content.project.all_languages.edit_predictions.get_or_insert_default().copilot = value;
- },
- }
- .unimplemented(),
- ),
- metadata: None,
- files: USER | PROJECT,
- }),
- SettingsPageItem::SettingItem(SettingItem {
- title: "Codestral Provider",
- description: "Use Mistral's Codestral as your edit prediction provider.",
- field: Box::new(
- SettingField {
- json_path: Some("edit_prediction.codestral_provider"),
- pick: |settings_content| {
- settings_content.project.all_languages.edit_predictions.as_ref()?.codestral.as_ref()
- },
- write: |settings_content, value| {
- settings_content.project.all_languages.edit_predictions.get_or_insert_default().codestral = value;
- },
- }
- .unimplemented(),
- ),
- metadata: None,
- files: USER | PROJECT,
- }),
]
);
items
@@ -7485,9 +7453,23 @@ fn non_editor_language_settings_data() -> Vec<SettingsPageItem> {
fn edit_prediction_language_settings_section() -> Vec<SettingsPageItem> {
vec![
SettingsPageItem::SectionHeader("Edit Predictions"),
+ SettingsPageItem::SubPageLink(SubPageLink {
+ title: "Configure Providers".into(),
+ json_path: Some("edit_predictions.providers"),
+ description: Some("Set up different edit prediction providers in complement to Zed's built-in Zeta model.".into()),
+ in_json: false,
+ files: USER,
+ render: Arc::new(|_, window, cx| {
+ let settings_window = cx.entity();
+ let page = window.use_state(cx, |_, _| {
+ crate::pages::EditPredictionSetupPage::new(settings_window)
+ });
+ page.into_any_element()
+ }),
+ }),
SettingsPageItem::SettingItem(SettingItem {
title: "Show Edit Predictions",
- description: "Controls whether edit predictions are shown immediately or manually by triggering `editor::showeditprediction` (false).",
+ description: "Controls whether edit predictions are shown immediately or manually.",
field: Box::new(SettingField {
json_path: Some("languages.$(language).show_edit_predictions"),
pick: |settings_content| {
@@ -7505,7 +7487,7 @@ fn edit_prediction_language_settings_section() -> Vec<SettingsPageItem> {
files: USER | PROJECT,
}),
SettingsPageItem::SettingItem(SettingItem {
- title: "Edit Predictions Disabled In",
+ title: "Disable in Language Scopes",
description: "Controls whether edit predictions are shown in the given language scopes.",
field: Box::new(
SettingField {
@@ -0,0 +1,2 @@
+mod edit_prediction_provider_setup;
+pub use edit_prediction_provider_setup::EditPredictionSetupPage;
@@ -0,0 +1,365 @@
+use edit_prediction::{
+ ApiKeyState, Zeta2FeatureFlag,
+ mercury::{MERCURY_CREDENTIALS_URL, mercury_api_token},
+ sweep_ai::{SWEEP_CREDENTIALS_URL, sweep_api_token},
+};
+use feature_flags::FeatureFlagAppExt as _;
+use gpui::{Entity, ScrollHandle, prelude::*};
+use language_models::provider::mistral::{CODESTRAL_API_URL, codestral_api_key};
+use ui::{ButtonLink, ConfiguredApiCard, WithScrollbar, prelude::*};
+
+use crate::{
+ SettingField, SettingItem, SettingsFieldMetadata, SettingsPageItem, SettingsWindow, USER,
+ components::{SettingsInputField, SettingsSectionHeader},
+};
+
+pub struct EditPredictionSetupPage {
+ settings_window: Entity<SettingsWindow>,
+ scroll_handle: ScrollHandle,
+}
+
+impl EditPredictionSetupPage {
+ pub fn new(settings_window: Entity<SettingsWindow>) -> Self {
+ Self {
+ settings_window,
+ scroll_handle: ScrollHandle::new(),
+ }
+ }
+}
+
+impl Render for EditPredictionSetupPage {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let settings_window = self.settings_window.clone();
+
+ let providers = [
+ Some(render_github_copilot_provider(window, cx).into_any_element()),
+ cx.has_flag::<Zeta2FeatureFlag>().then(|| {
+ render_api_key_provider(
+ IconName::Inception,
+ "Mercury",
+ "https://platform.inceptionlabs.ai/dashboard/api-keys".into(),
+ mercury_api_token(cx),
+ |_cx| MERCURY_CREDENTIALS_URL,
+ None,
+ window,
+ cx,
+ )
+ .into_any_element()
+ }),
+ cx.has_flag::<Zeta2FeatureFlag>().then(|| {
+ render_api_key_provider(
+ IconName::SweepAi,
+ "Sweep",
+ "https://app.sweep.dev/".into(),
+ sweep_api_token(cx),
+ |_cx| SWEEP_CREDENTIALS_URL,
+ None,
+ window,
+ cx,
+ )
+ .into_any_element()
+ }),
+ Some(
+ render_api_key_provider(
+ IconName::AiMistral,
+ "Codestral",
+ "https://console.mistral.ai/codestral".into(),
+ codestral_api_key(cx),
+ |cx| language_models::MistralLanguageModelProvider::api_url(cx),
+ Some(settings_window.update(cx, |settings_window, cx| {
+ let codestral_settings = codestral_settings();
+ settings_window
+ .render_sub_page_items_section(
+ codestral_settings.iter().enumerate(),
+ None,
+ window,
+ cx,
+ )
+ .into_any_element()
+ })),
+ window,
+ cx,
+ )
+ .into_any_element(),
+ ),
+ ];
+
+ div()
+ .size_full()
+ .vertical_scrollbar_for(&self.scroll_handle, window, cx)
+ .child(
+ v_flex()
+ .id("ep-setup-page")
+ .min_w_0()
+ .size_full()
+ .px_8()
+ .pb_16()
+ .overflow_y_scroll()
+ .track_scroll(&self.scroll_handle)
+ .children(providers.into_iter().flatten()),
+ )
+ }
+}
+
+fn render_api_key_provider(
+ icon: IconName,
+ title: &'static str,
+ link: SharedString,
+ api_key_state: Entity<ApiKeyState>,
+ current_url: fn(&mut App) -> SharedString,
+ additional_fields: Option<AnyElement>,
+ window: &mut Window,
+ cx: &mut Context<EditPredictionSetupPage>,
+) -> impl IntoElement {
+ let weak_page = cx.weak_entity();
+ _ = window.use_keyed_state(title, cx, |_, cx| {
+ let task = api_key_state.update(cx, |key_state, cx| {
+ key_state.load_if_needed(current_url(cx), |state| state, cx)
+ });
+ cx.spawn(async move |_, cx| {
+ task.await.ok();
+ weak_page
+ .update(cx, |_, cx| {
+ cx.notify();
+ })
+ .ok();
+ })
+ });
+
+ let (has_key, env_var_name, is_from_env_var) = api_key_state.read_with(cx, |state, _| {
+ (
+ state.has_key(),
+ Some(state.env_var_name().clone()),
+ state.is_from_env_var(),
+ )
+ });
+
+ let write_key = move |api_key: Option<String>, cx: &mut App| {
+ api_key_state
+ .update(cx, |key_state, cx| {
+ let url = current_url(cx);
+ key_state.store(url, api_key, |key_state| key_state, cx)
+ })
+ .detach_and_log_err(cx);
+ };
+
+ let base_container = v_flex().id(title).min_w_0().pt_8().gap_1p5();
+ let header = SettingsSectionHeader::new(title)
+ .icon(icon)
+ .no_padding(true);
+ let button_link_label = format!("{} dashboard", title);
+ let description = h_flex()
+ .min_w_0()
+ .gap_0p5()
+ .child(
+ Label::new("Visit the")
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ .child(
+ ButtonLink::new(button_link_label, link)
+ .no_icon(true)
+ .label_size(LabelSize::Small)
+ .label_color(Color::Muted),
+ )
+ .child(
+ Label::new("to generate an API key.")
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ );
+ let configured_card_label = if is_from_env_var {
+ "API Key Set in Environment Variable"
+ } else {
+ "API Key Configured"
+ };
+
+ let container = if has_key {
+ base_container.child(header).child(
+ ConfiguredApiCard::new(configured_card_label)
+ .button_label("Reset Key")
+ .button_tab_index(0)
+ .disabled(is_from_env_var)
+ .when_some(env_var_name, |this, env_var_name| {
+ this.when(is_from_env_var, |this| {
+ this.tooltip_label(format!(
+ "To reset your API key, unset the {} environment variable.",
+ env_var_name
+ ))
+ })
+ })
+ .on_click(move |_, _, cx| {
+ write_key(None, cx);
+ }),
+ )
+ } else {
+ base_container.child(header).child(
+ h_flex()
+ .pt_2p5()
+ .w_full()
+ .justify_between()
+ .child(
+ v_flex()
+ .w_full()
+ .max_w_1_2()
+ .child(Label::new("API Key"))
+ .child(description)
+ .when_some(env_var_name, |this, env_var_name| {
+ this.child({
+ let label = format!(
+ "Or set the {} env var and restart Zed.",
+ env_var_name.as_ref()
+ );
+ Label::new(label).size(LabelSize::Small).color(Color::Muted)
+ })
+ }),
+ )
+ .child(
+ SettingsInputField::new()
+ .tab_index(0)
+ .with_placeholder("xxxxxxxxxxxxxxxxxxxx")
+ .on_confirm(move |api_key, cx| {
+ write_key(api_key.filter(|key| !key.is_empty()), cx);
+ }),
+ ),
+ )
+ };
+
+ container.when_some(additional_fields, |this, additional_fields| {
+ this.child(
+ div()
+ .map(|this| if has_key { this.mt_1() } else { this.mt_4() })
+ .px_neg_8()
+ .border_t_1()
+ .border_color(cx.theme().colors().border_variant)
+ .child(additional_fields),
+ )
+ })
+}
+
+fn codestral_settings() -> Box<[SettingsPageItem]> {
+ Box::new([
+ SettingsPageItem::SettingItem(SettingItem {
+ title: "API URL",
+ description: "The API URL to use for Codestral.",
+ field: Box::new(SettingField {
+ pick: |settings| {
+ settings
+ .project
+ .all_languages
+ .edit_predictions
+ .as_ref()?
+ .codestral
+ .as_ref()?
+ .api_url
+ .as_ref()
+ },
+ write: |settings, value| {
+ settings
+ .project
+ .all_languages
+ .edit_predictions
+ .get_or_insert_default()
+ .codestral
+ .get_or_insert_default()
+ .api_url = value;
+ },
+ json_path: Some("edit_predictions.codestral.api_url"),
+ }),
+ metadata: Some(Box::new(SettingsFieldMetadata {
+ placeholder: Some(CODESTRAL_API_URL),
+ ..Default::default()
+ })),
+ files: USER,
+ }),
+ SettingsPageItem::SettingItem(SettingItem {
+ title: "Max Tokens",
+ description: "The maximum number of tokens to generate.",
+ field: Box::new(SettingField {
+ pick: |settings| {
+ settings
+ .project
+ .all_languages
+ .edit_predictions
+ .as_ref()?
+ .codestral
+ .as_ref()?
+ .max_tokens
+ .as_ref()
+ },
+ write: |settings, value| {
+ settings
+ .project
+ .all_languages
+ .edit_predictions
+ .get_or_insert_default()
+ .codestral
+ .get_or_insert_default()
+ .max_tokens = value;
+ },
+ json_path: Some("edit_predictions.codestral.max_tokens"),
+ }),
+ metadata: None,
+ files: USER,
+ }),
+ SettingsPageItem::SettingItem(SettingItem {
+ title: "Model",
+ description: "The Codestral model id to use.",
+ field: Box::new(SettingField {
+ pick: |settings| {
+ settings
+ .project
+ .all_languages
+ .edit_predictions
+ .as_ref()?
+ .codestral
+ .as_ref()?
+ .model
+ .as_ref()
+ },
+ write: |settings, value| {
+ settings
+ .project
+ .all_languages
+ .edit_predictions
+ .get_or_insert_default()
+ .codestral
+ .get_or_insert_default()
+ .model = value;
+ },
+ json_path: Some("edit_predictions.codestral.model"),
+ }),
+ metadata: Some(Box::new(SettingsFieldMetadata {
+ placeholder: Some("codestral-latest"),
+ ..Default::default()
+ })),
+ files: USER,
+ }),
+ ])
+}
+
+pub(crate) fn render_github_copilot_provider(
+ window: &mut Window,
+ cx: &mut App,
+) -> impl IntoElement {
+ let configuration_view = window.use_state(cx, |_, cx| {
+ copilot::ConfigurationView::new(
+ |cx| {
+ copilot::Copilot::global(cx)
+ .is_some_and(|copilot| copilot.read(cx).is_authenticated())
+ },
+ copilot::ConfigurationMode::EditPrediction,
+ cx,
+ )
+ });
+
+ v_flex()
+ .id("github-copilot")
+ .min_w_0()
+ .gap_1p5()
+ .child(
+ SettingsSectionHeader::new("GitHub Copilot")
+ .icon(IconName::Copilot)
+ .no_padding(true),
+ )
+ .child(configuration_view)
+}
@@ -1,5 +1,6 @@
mod components;
mod page_data;
+mod pages;
use anyhow::Result;
use editor::{Editor, EditorEvent};
@@ -28,9 +29,8 @@ use std::{
};
use title_bar::platform_title_bar::PlatformTitleBar;
use ui::{
- Banner, ContextMenu, Divider, DividerColor, DropdownMenu, DropdownStyle, IconButtonShape,
- KeyBinding, KeybindingHint, PopoverMenu, Switch, Tooltip, TreeViewItem, WithScrollbar,
- prelude::*,
+ Banner, ContextMenu, Divider, DropdownMenu, DropdownStyle, IconButtonShape, KeyBinding,
+ KeybindingHint, PopoverMenu, Switch, Tooltip, TreeViewItem, WithScrollbar, prelude::*,
};
use ui_input::{NumberField, NumberFieldType};
use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
@@ -38,7 +38,8 @@ use workspace::{AppState, OpenOptions, OpenVisible, Workspace, client_side_decor
use zed_actions::{OpenProjectSettings, OpenSettings, OpenSettingsAt};
use crate::components::{
- EnumVariantDropdown, SettingsInputField, font_picker, icon_theme_picker, theme_picker,
+ EnumVariantDropdown, SettingsInputField, SettingsSectionHeader, font_picker, icon_theme_picker,
+ theme_picker,
};
const NAVBAR_CONTAINER_TAB_INDEX: isize = 0;
@@ -613,7 +614,10 @@ pub fn open_settings_editor(
app_id: Some(app_id.to_owned()),
window_decorations: Some(window_decorations),
window_min_size: Some(gpui::Size {
- width: px(360.0),
+ // Don't make the settings window thinner than this,
+ // otherwise, it gets unusable. Users with smaller res monitors
+ // can customize the height, but not the width.
+ width: px(900.0),
height: px(240.0),
}),
window_bounds: Some(WindowBounds::centered(scaled_bounds, cx)),
@@ -834,18 +838,9 @@ impl SettingsPageItem {
};
match self {
- SettingsPageItem::SectionHeader(header) => v_flex()
- .w_full()
- .px_8()
- .gap_1p5()
- .child(
- Label::new(SharedString::new_static(header))
- .size(LabelSize::Small)
- .color(Color::Muted)
- .buffer_font(cx),
- )
- .child(Divider::horizontal().color(DividerColor::BorderFaded))
- .into_any_element(),
+ SettingsPageItem::SectionHeader(header) => {
+ SettingsSectionHeader::new(SharedString::new_static(header)).into_any_element()
+ }
SettingsPageItem::SettingItem(setting_item) => {
let (field_with_padding, _) =
render_setting_item_inner(setting_item, true, false, cx);
@@ -869,9 +864,20 @@ impl SettingsPageItem {
.map(apply_padding)
.child(
v_flex()
+ .relative()
.w_full()
.max_w_1_2()
- .child(Label::new(sub_page_link.title.clone())),
+ .child(Label::new(sub_page_link.title.clone()))
+ .when_some(
+ sub_page_link.description.as_ref(),
+ |this, description| {
+ this.child(
+ Label::new(description.clone())
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ },
+ ),
)
.child(
Button::new(
@@ -909,7 +915,13 @@ impl SettingsPageItem {
this.push_sub_page(sub_page_link.clone(), header, cx)
})
}),
- ),
+ )
+ .child(render_settings_item_link(
+ sub_page_link.title.clone(),
+ sub_page_link.json_path,
+ false,
+ cx,
+ )),
)
.when(!is_last, |this| this.child(Divider::horizontal()))
.into_any_element(),
@@ -983,20 +995,6 @@ fn render_settings_item(
let (found_in_file, _) = setting_item.field.file_set_in(file.clone(), cx);
let file_set_in = SettingsUiFile::from_settings(found_in_file.clone());
- let clipboard_has_link = cx
- .read_from_clipboard()
- .and_then(|entry| entry.text())
- .map_or(false, |maybe_url| {
- setting_item.field.json_path().is_some()
- && maybe_url.strip_prefix("zed://settings/") == setting_item.field.json_path()
- });
-
- let (link_icon, link_icon_color) = if clipboard_has_link {
- (IconName::Check, Color::Success)
- } else {
- (IconName::Link, Color::Muted)
- };
-
h_flex()
.id(setting_item.title)
.min_w_0()
@@ -1056,40 +1054,60 @@ fn render_settings_item(
)
.child(control)
.when(sub_page_stack().is_empty(), |this| {
- // Intentionally using the description to make the icon button
- // unique because some items share the same title (e.g., "Font Size")
- let icon_button_id =
- SharedString::new(format!("copy-link-btn-{}", setting_item.description));
+ this.child(render_settings_item_link(
+ setting_item.description,
+ setting_item.field.json_path(),
+ sub_field,
+ cx,
+ ))
+ })
+}
- this.child(
- div()
- .absolute()
- .top(rems_from_px(18.))
- .map(|this| {
- if sub_field {
- this.visible_on_hover("setting-sub-item")
- .left(rems_from_px(-8.5))
- } else {
- this.visible_on_hover("setting-item")
- .left(rems_from_px(-22.))
- }
- })
- .child({
- IconButton::new(icon_button_id, link_icon)
- .icon_color(link_icon_color)
- .icon_size(IconSize::Small)
- .shape(IconButtonShape::Square)
- .tooltip(Tooltip::text("Copy Link"))
- .when_some(setting_item.field.json_path(), |this, path| {
- this.on_click(cx.listener(move |_, _, _, cx| {
- let link = format!("zed://settings/{}", path);
- cx.write_to_clipboard(ClipboardItem::new_string(link));
- cx.notify();
- }))
- })
- }),
- )
+fn render_settings_item_link(
+ id: impl Into<ElementId>,
+ json_path: Option<&'static str>,
+ sub_field: bool,
+ cx: &mut Context<'_, SettingsWindow>,
+) -> impl IntoElement {
+ let clipboard_has_link = cx
+ .read_from_clipboard()
+ .and_then(|entry| entry.text())
+ .map_or(false, |maybe_url| {
+ json_path.is_some() && maybe_url.strip_prefix("zed://settings/") == json_path
+ });
+
+ let (link_icon, link_icon_color) = if clipboard_has_link {
+ (IconName::Check, Color::Success)
+ } else {
+ (IconName::Link, Color::Muted)
+ };
+
+ div()
+ .absolute()
+ .top(rems_from_px(18.))
+ .map(|this| {
+ if sub_field {
+ this.visible_on_hover("setting-sub-item")
+ .left(rems_from_px(-8.5))
+ } else {
+ this.visible_on_hover("setting-item")
+ .left(rems_from_px(-22.))
+ }
})
+ .child(
+ IconButton::new((id.into(), "copy-link-btn"), link_icon)
+ .icon_color(link_icon_color)
+ .icon_size(IconSize::Small)
+ .shape(IconButtonShape::Square)
+ .tooltip(Tooltip::text("Copy Link"))
+ .when_some(json_path, |this, path| {
+ this.on_click(cx.listener(move |_, _, _, cx| {
+ let link = format!("zed://settings/{}", path);
+ cx.write_to_clipboard(ClipboardItem::new_string(link));
+ cx.notify();
+ }))
+ }),
+ )
}
struct SettingItem {
@@ -1175,6 +1193,12 @@ impl PartialEq for SettingItem {
#[derive(Clone)]
struct SubPageLink {
title: SharedString,
+ description: Option<SharedString>,
+ /// See [`SettingField.json_path`]
+ json_path: Option<&'static str>,
+ /// Whether or not the settings in this sub page are configurable in settings.json
+ /// Removes the "Edit in settings.json" button from the page.
+ in_json: bool,
files: FileMask,
render: Arc<
dyn Fn(&mut SettingsWindow, &mut Window, &mut Context<SettingsWindow>) -> AnyElement
@@ -1835,6 +1859,7 @@ impl SettingsWindow {
header_str = *header;
}
SettingsPageItem::SubPageLink(sub_page_link) => {
+ json_path = sub_page_link.json_path;
documents.push(bm25::Document {
id: key_index,
contents: [page.title, header_str, sub_page_link.title.as_ref()]
@@ -2758,19 +2783,49 @@ impl SettingsWindow {
page_content
}
- fn render_sub_page_items<'a, Items: Iterator<Item = (usize, &'a SettingsPageItem)>>(
+ fn render_sub_page_items<'a, Items>(
&self,
items: Items,
page_index: Option<usize>,
window: &mut Window,
cx: &mut Context<SettingsWindow>,
- ) -> impl IntoElement {
- let mut page_content = v_flex()
+ ) -> impl IntoElement
+ where
+ Items: Iterator<Item = (usize, &'a SettingsPageItem)>,
+ {
+ let page_content = v_flex()
.id("settings-ui-page")
.size_full()
.overflow_y_scroll()
.track_scroll(&self.sub_page_scroll_handle);
+ self.render_sub_page_items_in(page_content, items, page_index, window, cx)
+ }
+ fn render_sub_page_items_section<'a, Items>(
+ &self,
+ items: Items,
+ page_index: Option<usize>,
+ window: &mut Window,
+ cx: &mut Context<SettingsWindow>,
+ ) -> impl IntoElement
+ where
+ Items: Iterator<Item = (usize, &'a SettingsPageItem)>,
+ {
+ let page_content = v_flex().id("settings-ui-sub-page-section").size_full();
+ self.render_sub_page_items_in(page_content, items, page_index, window, cx)
+ }
+
+ fn render_sub_page_items_in<'a, Items>(
+ &self,
+ mut page_content: Stateful<Div>,
+ items: Items,
+ page_index: Option<usize>,
+ window: &mut Window,
+ cx: &mut Context<SettingsWindow>,
+ ) -> impl IntoElement
+ where
+ Items: Iterator<Item = (usize, &'a SettingsPageItem)>,
+ {
let items: Vec<_> = items.collect();
let items_len = items.len();
let mut section_header = None;
@@ -2871,18 +2926,25 @@ impl SettingsWindow {
)
.child(self.render_sub_page_breadcrumbs()),
)
- .child(
- Button::new("open-in-settings-file", "Edit in settings.json")
- .tab_index(0_isize)
- .style(ButtonStyle::OutlinedGhost)
- .tooltip(Tooltip::for_action_title_in(
- "Edit in settings.json",
- &OpenCurrentFile,
- &self.focus_handle,
- ))
- .on_click(cx.listener(|this, _, window, cx| {
- this.open_current_settings_file(window, cx);
- })),
+ .when(
+ sub_page_stack()
+ .last()
+ .is_none_or(|sub_page| sub_page.link.in_json),
+ |this| {
+ this.child(
+ Button::new("open-in-settings-file", "Edit in settings.json")
+ .tab_index(0_isize)
+ .style(ButtonStyle::OutlinedGhost)
+ .tooltip(Tooltip::for_action_title_in(
+ "Edit in settings.json",
+ &OpenCurrentFile,
+ &self.focus_handle,
+ ))
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.open_current_settings_file(window, cx);
+ })),
+ )
+ },
)
.into_any_element();
@@ -1,3 +1,4 @@
+mod ai;
mod avatar;
mod banner;
mod button;
@@ -16,6 +17,7 @@ mod icon;
mod image;
mod indent_guides;
mod indicator;
+mod inline_code;
mod keybinding;
mod keybinding_hint;
mod label;
@@ -43,6 +45,7 @@ mod tree_view_item;
#[cfg(feature = "stories")]
mod stories;
+pub use ai::*;
pub use avatar::*;
pub use banner::*;
pub use button::*;
@@ -61,6 +64,7 @@ pub use icon::*;
pub use image::*;
pub use indent_guides::*;
pub use indicator::*;
+pub use inline_code::*;
pub use keybinding::*;
pub use keybinding_hint::*;
pub use label::*;
@@ -0,0 +1,3 @@
+mod configured_api_card;
+
+pub use configured_api_card::*;
@@ -1,10 +1,11 @@
+use crate::{Tooltip, prelude::*};
use gpui::{ClickEvent, IntoElement, ParentElement, SharedString};
-use ui::{Tooltip, prelude::*};
#[derive(IntoElement)]
pub struct ConfiguredApiCard {
label: SharedString,
button_label: Option<SharedString>,
+ button_tab_index: Option<isize>,
tooltip_label: Option<SharedString>,
disabled: bool,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
@@ -15,6 +16,7 @@ impl ConfiguredApiCard {
Self {
label: label.into(),
button_label: None,
+ button_tab_index: None,
tooltip_label: None,
disabled: false,
on_click: None,
@@ -43,6 +45,11 @@ impl ConfiguredApiCard {
self.disabled = disabled;
self
}
+
+ pub fn button_tab_index(mut self, tab_index: isize) -> Self {
+ self.button_tab_index = Some(tab_index);
+ self
+ }
}
impl RenderOnce for ConfiguredApiCard {
@@ -51,23 +58,27 @@ impl RenderOnce for ConfiguredApiCard {
let button_id = SharedString::new(format!("id-{}", button_label));
h_flex()
+ .min_w_0()
.mt_0p5()
.p_1()
.justify_between()
.rounded_md()
+ .flex_wrap()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().background)
.child(
h_flex()
- .flex_1()
.min_w_0()
.gap_1()
.child(Icon::new(IconName::Check).color(Color::Success))
- .child(Label::new(self.label).truncate()),
+ .child(Label::new(self.label)),
)
.child(
Button::new(button_id, button_label)
+ .when_some(self.button_tab_index, |elem, tab_index| {
+ elem.tab_index(tab_index)
+ })
.label_size(LabelSize::Small)
.icon(IconName::Undo)
.icon_size(IconSize::Small)
@@ -1,12 +1,14 @@
mod button;
mod button_icon;
mod button_like;
+mod button_link;
mod icon_button;
mod split_button;
mod toggle_button;
pub use button::*;
pub use button_like::*;
+pub use button_link::*;
pub use icon_button::*;
pub use split_button::*;
pub use toggle_button::*;
@@ -0,0 +1,102 @@
+use gpui::{IntoElement, Window, prelude::*};
+
+use crate::{ButtonLike, prelude::*};
+
+/// A button that takes an underline to look like a regular web link.
+/// It also contains an arrow icon to communicate the link takes you out of Zed.
+///
+/// # Usage Example
+///
+/// ```
+/// use ui::ButtonLink;
+///
+/// let button_link = ButtonLink::new("Click me", "https://example.com");
+/// ```
+#[derive(IntoElement, RegisterComponent)]
+pub struct ButtonLink {
+ label: SharedString,
+ label_size: LabelSize,
+ label_color: Color,
+ link: String,
+ no_icon: bool,
+}
+
+impl ButtonLink {
+ pub fn new(label: impl Into<SharedString>, link: impl Into<String>) -> Self {
+ Self {
+ link: link.into(),
+ label: label.into(),
+ label_size: LabelSize::Default,
+ label_color: Color::Default,
+ no_icon: false,
+ }
+ }
+
+ pub fn no_icon(mut self, no_icon: bool) -> Self {
+ self.no_icon = no_icon;
+ self
+ }
+
+ pub fn label_size(mut self, label_size: LabelSize) -> Self {
+ self.label_size = label_size;
+ self
+ }
+
+ pub fn label_color(mut self, label_color: Color) -> Self {
+ self.label_color = label_color;
+ self
+ }
+}
+
+impl RenderOnce for ButtonLink {
+ fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
+ let id = format!("{}-{}", self.label, self.link);
+
+ ButtonLike::new(id)
+ .size(ButtonSize::None)
+ .child(
+ h_flex()
+ .gap_0p5()
+ .child(
+ Label::new(self.label)
+ .size(self.label_size)
+ .color(self.label_color)
+ .underline(),
+ )
+ .when(!self.no_icon, |this| {
+ this.child(
+ Icon::new(IconName::ArrowUpRight)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
+ }),
+ )
+ .on_click(move |_, _, cx| cx.open_url(&self.link))
+ .into_any_element()
+ }
+}
+
+impl Component for ButtonLink {
+ fn scope() -> ComponentScope {
+ ComponentScope::Navigation
+ }
+
+ fn description() -> Option<&'static str> {
+ Some("A button that opens a URL.")
+ }
+
+ fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
+ Some(
+ v_flex()
+ .gap_6()
+ .child(
+ example_group(vec![single_example(
+ "Simple",
+ ButtonLink::new("zed.dev", "https://zed.dev").into_any_element(),
+ )])
+ .vertical(),
+ )
+ .into_any_element(),
+ )
+ }
+}
@@ -144,12 +144,18 @@ impl Divider {
impl RenderOnce for Divider {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let base = match self.direction {
- DividerDirection::Horizontal => {
- div().h_px().w_full().when(self.inset, |this| this.mx_1p5())
- }
- DividerDirection::Vertical => {
- div().w_px().h_full().when(self.inset, |this| this.my_1p5())
- }
+ DividerDirection::Horizontal => div()
+ .min_w_0()
+ .flex_none()
+ .h_px()
+ .w_full()
+ .when(self.inset, |this| this.mx_1p5()),
+ DividerDirection::Vertical => div()
+ .min_w_0()
+ .flex_none()
+ .w_px()
+ .h_full()
+ .when(self.inset, |this| this.my_1p5()),
};
match self.style {
@@ -0,0 +1,64 @@
+use crate::prelude::*;
+use gpui::{AnyElement, IntoElement, ParentElement, Styled};
+
+/// InlineCode mimics the way inline code is rendered when wrapped in backticks in Markdown.
+///
+/// # Usage Example
+///
+/// ```
+/// use ui::InlineCode;
+///
+/// let InlineCode = InlineCode::new("<div>hey</div>");
+/// ```
+#[derive(IntoElement, RegisterComponent)]
+pub struct InlineCode {
+ label: SharedString,
+ label_size: LabelSize,
+}
+
+impl InlineCode {
+ pub fn new(label: impl Into<SharedString>) -> Self {
+ Self {
+ label: label.into(),
+ label_size: LabelSize::Default,
+ }
+ }
+
+ /// Sets the size of the label.
+ pub fn label_size(mut self, size: LabelSize) -> Self {
+ self.label_size = size;
+ self
+ }
+}
+
+impl RenderOnce for InlineCode {
+ fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
+ h_flex()
+ .min_w_0()
+ .px_0p5()
+ .overflow_hidden()
+ .bg(cx.theme().colors().text.opacity(0.05))
+ .child(Label::new(self.label).size(self.label_size).buffer_font(cx))
+ }
+}
+
+impl Component for InlineCode {
+ fn scope() -> ComponentScope {
+ ComponentScope::DataDisplay
+ }
+
+ fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
+ Some(
+ v_flex()
+ .gap_6()
+ .child(
+ example_group(vec![single_example(
+ "Simple",
+ InlineCode::new("zed.dev").into_any_element(),
+ )])
+ .vertical(),
+ )
+ .into_any_element(),
+ )
+ }
+}
@@ -227,7 +227,7 @@ impl RenderOnce for LabelLike {
.get_or_insert_with(Default::default)
.underline = Some(UnderlineStyle {
thickness: px(1.),
- color: None,
+ color: Some(cx.theme().colors().text_muted.opacity(0.4)),
wavy: false,
});
this
@@ -1,18 +1,33 @@
-use crate::{ListItem, prelude::*};
-use component::{Component, ComponentScope, example_group_with_title, single_example};
+use crate::{ButtonLink, ListItem, prelude::*};
+use component::{Component, ComponentScope, example_group, single_example};
use gpui::{IntoElement, ParentElement, SharedString};
#[derive(IntoElement, RegisterComponent)]
pub struct ListBulletItem {
label: SharedString,
+ label_color: Option<Color>,
+ children: Vec<AnyElement>,
}
impl ListBulletItem {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
label: label.into(),
+ label_color: None,
+ children: Vec::new(),
}
}
+
+ pub fn label_color(mut self, color: Color) -> Self {
+ self.label_color = Some(color);
+ self
+ }
+}
+
+impl ParentElement for ListBulletItem {
+ fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
+ self.children.extend(elements)
+ }
}
impl RenderOnce for ListBulletItem {
@@ -34,7 +49,18 @@ impl RenderOnce for ListBulletItem {
.color(Color::Hidden),
),
)
- .child(div().w_full().min_w_0().child(Label::new(self.label))),
+ .map(|this| {
+ if !self.children.is_empty() {
+ this.child(h_flex().gap_0p5().flex_wrap().children(self.children))
+ } else {
+ this.child(
+ div().w_full().min_w_0().child(
+ Label::new(self.label)
+ .color(self.label_color.unwrap_or(Color::Default)),
+ ),
+ )
+ }
+ }),
)
.into_any_element()
}
@@ -46,37 +72,43 @@ impl Component for ListBulletItem {
}
fn description() -> Option<&'static str> {
- Some("A list item with a bullet point indicator for unordered lists.")
+ Some("A list item with a dash indicator for unordered lists.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
+ let basic_examples = vec![
+ single_example(
+ "Simple",
+ ListBulletItem::new("First bullet item").into_any_element(),
+ ),
+ single_example(
+ "Multiple Lines",
+ v_flex()
+ .child(ListBulletItem::new("First item"))
+ .child(ListBulletItem::new("Second item"))
+ .child(ListBulletItem::new("Third item"))
+ .into_any_element(),
+ ),
+ single_example(
+ "Long Text",
+ ListBulletItem::new(
+ "A longer bullet item that demonstrates text wrapping behavior",
+ )
+ .into_any_element(),
+ ),
+ single_example(
+ "With Link",
+ ListBulletItem::new("")
+ .child(Label::new("Create a Zed account by"))
+ .child(ButtonLink::new("visiting the website", "https://zed.dev"))
+ .into_any_element(),
+ ),
+ ];
+
Some(
v_flex()
.gap_6()
- .child(example_group_with_title(
- "Bullet Items",
- vec![
- single_example(
- "Simple",
- ListBulletItem::new("First bullet item").into_any_element(),
- ),
- single_example(
- "Multiple Lines",
- v_flex()
- .child(ListBulletItem::new("First item"))
- .child(ListBulletItem::new("Second item"))
- .child(ListBulletItem::new("Third item"))
- .into_any_element(),
- ),
- single_example(
- "Long Text",
- ListBulletItem::new(
- "A longer bullet item that demonstrates text wrapping behavior",
- )
- .into_any_element(),
- ),
- ],
- ))
+ .child(example_group(basic_examples).vertical())
.into_any_element(),
)
}
@@ -41,7 +41,7 @@ pub enum NotificationId {
impl NotificationId {
/// Returns a unique [`NotificationId`] for the given type.
- pub fn unique<T: 'static>() -> Self {
+ pub const fn unique<T: 'static>() -> Self {
Self::Unique(TypeId::of::<T>())
}
@@ -5,6 +5,7 @@ use std::sync::LazyLock;
/// When true, Zed will use in-memory databases instead of persistent storage.
pub static ZED_STATELESS: LazyLock<bool> = bool_env_var!("ZED_STATELESS");
+#[derive(Clone)]
pub struct EnvVar {
pub name: SharedString,
/// Value of the environment variable. Also `None` when set to an empty string.
@@ -30,7 +31,7 @@ impl EnvVar {
#[macro_export]
macro_rules! env_var {
($name:expr) => {
- LazyLock::new(|| $crate::EnvVar::new(($name).into()))
+ ::std::sync::LazyLock::new(|| $crate::EnvVar::new(($name).into()))
};
}
@@ -39,6 +40,6 @@ macro_rules! env_var {
#[macro_export]
macro_rules! bool_env_var {
($name:expr) => {
- LazyLock::new(|| $crate::EnvVar::new(($name).into()).value.is_some())
+ ::std::sync::LazyLock::new(|| $crate::EnvVar::new(($name).into()).value.is_some())
};
}