, cx: &mut TestAppContext) -> Entity>);
let node_runtime = NodeRuntime::new(
http_client.clone(),
Some(shell_env_loaded_rx),
node_settings_rx,
+ trust_task,
);
let mut languages = LanguageRegistry::new(cx.background_executor().clone());
@@ -468,6 +474,7 @@ pub fn execute_run(
languages,
extension_host_proxy,
},
+ true,
cx,
)
});
diff --git a/crates/settings/src/settings_content/project.rs b/crates/settings/src/settings_content/project.rs
index 5cd708694d0cfd3699fdc822509d0209f9a96fd1..a5e15153832c425134e129cba1984b3b5886aa56 100644
--- a/crates/settings/src/settings_content/project.rs
+++ b/crates/settings/src/settings_content/project.rs
@@ -187,6 +187,12 @@ pub struct SessionSettingsContent {
///
/// Default: true
pub restore_unsaved_buffers: Option,
+ /// Whether or not to skip worktree trust checks.
+ /// When trusted, project settings are synchronized automatically,
+ /// language and MCP servers are downloaded and started automatically.
+ ///
+ /// Default: false
+ pub trust_all_worktrees: Option,
}
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, MergeFrom, Debug)]
diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs
index 007c41ad59b4e875770beecb089bd4e7fb2078b5..1d0603de3184ad9da874b428a94af37d8966e6a2 100644
--- a/crates/settings_ui/src/page_data.rs
+++ b/crates/settings_ui/src/page_data.rs
@@ -138,6 +138,28 @@ pub(crate) fn settings_data(cx: &App) -> Vec {
metadata: None,
files: USER,
}),
+ SettingsPageItem::SectionHeader("Security"),
+ SettingsPageItem::SettingItem(SettingItem {
+ title: "Trust All Projects By Default",
+ description: "When opening Zed, avoid Restricted Mode by auto-trusting all projects, enabling use of all features without having to give permission to each new project.",
+ field: Box::new(SettingField {
+ json_path: Some("session.trust_all_projects"),
+ pick: |settings_content| {
+ settings_content
+ .session
+ .as_ref()
+ .and_then(|session| session.trust_all_worktrees.as_ref())
+ },
+ write: |settings_content, value| {
+ settings_content
+ .session
+ .get_or_insert_default()
+ .trust_all_worktrees = value;
+ },
+ }),
+ metadata: None,
+ files: USER,
+ }),
SettingsPageItem::SectionHeader("Workspace Restoration"),
SettingsPageItem::SettingItem(SettingItem {
title: "Restore Unsaved Buffers",
diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs
index 4d7397a0bc82142245b86c11ffdf441a6b781ad8..608fea7383176460cb4b7519824cd2dc118dbb69 100644
--- a/crates/title_bar/src/title_bar.rs
+++ b/crates/title_bar/src/title_bar.rs
@@ -30,18 +30,20 @@ use gpui::{
Subscription, WeakEntity, Window, actions, div,
};
use onboarding_banner::OnboardingBanner;
-use project::{Project, WorktreeSettings, git_store::GitStoreEvent};
+use project::{
+ Project, WorktreeSettings, git_store::GitStoreEvent, trusted_worktrees::TrustedWorktrees,
+};
use remote::RemoteConnectionOptions;
use settings::{Settings, SettingsLocation};
use std::sync::Arc;
use theme::ActiveTheme;
use title_bar_settings::TitleBarSettings;
use ui::{
- Avatar, Button, ButtonLike, ButtonStyle, Chip, ContextMenu, Icon, IconName, IconSize,
- IconWithIndicator, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, h_flex, prelude::*,
+ Avatar, ButtonLike, Chip, ContextMenu, IconWithIndicator, Indicator, PopoverMenu,
+ PopoverMenuHandle, TintColor, Tooltip, prelude::*,
};
use util::{ResultExt, rel_path::RelPath};
-use workspace::{Workspace, notifications::NotifyResultExt};
+use workspace::{ToggleWorktreeSecurity, Workspace, notifications::NotifyResultExt};
use zed_actions::{OpenRecent, OpenRemote};
pub use onboarding_banner::restore_banner;
@@ -163,6 +165,7 @@ impl Render for TitleBar {
title_bar
.when(title_bar_settings.show_project_items, |title_bar| {
title_bar
+ .children(self.render_restricted_mode(cx))
.children(self.render_project_host(cx))
.child(self.render_project_name(cx))
})
@@ -291,7 +294,12 @@ impl TitleBar {
_ => {}
}),
);
- subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
+ subscriptions.push(cx.observe(&user_store, |_a, _, cx| cx.notify()));
+ if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
+ subscriptions.push(cx.subscribe(&trusted_worktrees, |_, _, _, cx| {
+ cx.notify();
+ }));
+ }
let banner = cx.new(|cx| {
OnboardingBanner::new(
@@ -317,7 +325,7 @@ impl TitleBar {
client,
_subscriptions: subscriptions,
banner,
- screen_share_popover_handle: Default::default(),
+ screen_share_popover_handle: PopoverMenuHandle::default(),
}
}
@@ -427,6 +435,48 @@ impl TitleBar {
)
}
+ pub fn render_restricted_mode(&self, cx: &mut Context) -> Option {
+ let has_restricted_worktrees = TrustedWorktrees::try_get_global(cx)
+ .map(|trusted_worktrees| {
+ trusted_worktrees
+ .read(cx)
+ .has_restricted_worktrees(&self.project.read(cx).worktree_store(), cx)
+ })
+ .unwrap_or(false);
+ if !has_restricted_worktrees {
+ return None;
+ }
+
+ Some(
+ Button::new("restricted_mode_trigger", "Restricted Mode")
+ .style(ButtonStyle::Tinted(TintColor::Warning))
+ .label_size(LabelSize::Small)
+ .color(Color::Warning)
+ .icon(IconName::Warning)
+ .icon_color(Color::Warning)
+ .icon_size(IconSize::Small)
+ .icon_position(IconPosition::Start)
+ .tooltip(|_, cx| {
+ Tooltip::with_meta(
+ "You're in Restricted Mode",
+ Some(&ToggleWorktreeSecurity),
+ "Mark this project as trusted and unlock all features",
+ cx,
+ )
+ })
+ .on_click({
+ cx.listener(move |this, _, window, cx| {
+ this.workspace
+ .update(cx, |workspace, cx| {
+ workspace.show_worktree_trust_security_modal(true, window, cx)
+ })
+ .log_err();
+ })
+ })
+ .into_any_element(),
+ )
+ }
+
pub fn render_project_host(&self, cx: &mut Context) -> Option {
if self.project.read(cx).is_via_remote_server() {
return self.render_remote_project_connection(cx);
diff --git a/crates/ui/src/components/notification/alert_modal.rs b/crates/ui/src/components/notification/alert_modal.rs
index 9990dc1ce5f13e6834a009c4b8d7c14b594ccf36..52a084c847887a4dea7fd8b9a3fbad8390f68863 100644
--- a/crates/ui/src/components/notification/alert_modal.rs
+++ b/crates/ui/src/components/notification/alert_modal.rs
@@ -1,73 +1,161 @@
use crate::component_prelude::*;
use crate::prelude::*;
+use crate::{Checkbox, ListBulletItem, ToggleState};
+use gpui::Action;
+use gpui::FocusHandle;
use gpui::IntoElement;
+use gpui::Stateful;
use smallvec::{SmallVec, smallvec};
+use theme::ActiveTheme;
+
+type ActionHandler = Box) -> Stateful>;
#[derive(IntoElement, RegisterComponent)]
pub struct AlertModal {
id: ElementId,
+ header: Option
,
children: SmallVec<[AnyElement; 2]>,
- title: SharedString,
- primary_action: SharedString,
- dismiss_label: SharedString,
+ footer: Option,
+ title: Option,
+ primary_action: Option,
+ dismiss_label: Option,
+ width: Option,
+ key_context: Option,
+ action_handlers: Vec,
+ focus_handle: Option,
}
impl AlertModal {
- pub fn new(id: impl Into, title: impl Into) -> Self {
+ pub fn new(id: impl Into) -> Self {
Self {
id: id.into(),
+ header: None,
children: smallvec![],
- title: title.into(),
- primary_action: "Ok".into(),
- dismiss_label: "Cancel".into(),
+ footer: None,
+ title: None,
+ primary_action: None,
+ dismiss_label: None,
+ width: None,
+ key_context: None,
+ action_handlers: Vec::new(),
+ focus_handle: None,
}
}
+ pub fn title(mut self, title: impl Into) -> Self {
+ self.title = Some(title.into());
+ self
+ }
+
+ pub fn header(mut self, header: impl IntoElement) -> Self {
+ self.header = Some(header.into_any_element());
+ self
+ }
+
+ pub fn footer(mut self, footer: impl IntoElement) -> Self {
+ self.footer = Some(footer.into_any_element());
+ self
+ }
+
pub fn primary_action(mut self, primary_action: impl Into) -> Self {
- self.primary_action = primary_action.into();
+ self.primary_action = Some(primary_action.into());
self
}
pub fn dismiss_label(mut self, dismiss_label: impl Into) -> Self {
- self.dismiss_label = dismiss_label.into();
+ self.dismiss_label = Some(dismiss_label.into());
+ self
+ }
+
+ pub fn width(mut self, width: impl Into) -> Self {
+ self.width = Some(width.into());
+ self
+ }
+
+ pub fn key_context(mut self, key_context: impl Into) -> Self {
+ self.key_context = Some(key_context.into());
+ self
+ }
+
+ pub fn on_action(
+ mut self,
+ listener: impl Fn(&A, &mut Window, &mut App) + 'static,
+ ) -> Self {
+ self.action_handlers
+ .push(Box::new(move |div| div.on_action(listener)));
+ self
+ }
+
+ pub fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self {
+ self.focus_handle = Some(focus_handle.clone());
self
}
}
impl RenderOnce for AlertModal {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
- v_flex()
+ let width = self.width.unwrap_or_else(|| px(440.).into());
+ let has_default_footer = self.primary_action.is_some() || self.dismiss_label.is_some();
+
+ let mut modal = v_flex()
+ .when_some(self.key_context, |this, key_context| {
+ this.key_context(key_context.as_str())
+ })
+ .when_some(self.focus_handle, |this, focus_handle| {
+ this.track_focus(&focus_handle)
+ })
.id(self.id)
.elevation_3(cx)
- .w(px(440.))
- .p_5()
- .child(
+ .w(width)
+ .bg(cx.theme().colors().elevated_surface_background)
+ .overflow_hidden();
+
+ for handler in self.action_handlers {
+ modal = handler(modal);
+ }
+
+ if let Some(header) = self.header {
+ modal = modal.child(header);
+ } else if let Some(title) = self.title {
+ modal = modal.child(
+ v_flex()
+ .pt_3()
+ .pr_3()
+ .pl_3()
+ .pb_1()
+ .child(Headline::new(title).size(HeadlineSize::Small)),
+ );
+ }
+
+ if !self.children.is_empty() {
+ modal = modal.child(
v_flex()
+ .p_3()
.text_ui(cx)
.text_color(Color::Muted.color(cx))
.gap_1()
- .child(Headline::new(self.title).size(HeadlineSize::Small))
.children(self.children),
- )
- .child(
+ );
+ }
+
+ if let Some(footer) = self.footer {
+ modal = modal.child(footer);
+ } else if has_default_footer {
+ let primary_action = self.primary_action.unwrap_or_else(|| "Ok".into());
+ let dismiss_label = self.dismiss_label.unwrap_or_else(|| "Cancel".into());
+
+ modal = modal.child(
h_flex()
- .h(rems(1.75))
+ .p_3()
.items_center()
- .child(div().flex_1())
- .child(
- h_flex()
- .items_center()
- .gap_1()
- .child(
- Button::new(self.dismiss_label.clone(), self.dismiss_label.clone())
- .color(Color::Muted),
- )
- .child(Button::new(
- self.primary_action.clone(),
- self.primary_action,
- )),
- ),
- )
+ .justify_end()
+ .gap_1()
+ .child(Button::new(dismiss_label.clone(), dismiss_label).color(Color::Muted))
+ .child(Button::new(primary_action.clone(), primary_action)),
+ );
+ }
+
+ modal
}
}
@@ -90,24 +178,75 @@ impl Component for AlertModal {
Some("A modal dialog that presents an alert message with primary and dismiss actions.")
}
- fn preview(_window: &mut Window, _cx: &mut App) -> Option {
+ fn preview(_window: &mut Window, cx: &mut App) -> Option {
Some(
v_flex()
.gap_6()
.p_4()
- .children(vec![example_group(
- vec![
- single_example(
- "Basic Alert",
- AlertModal::new("simple-modal", "Do you want to leave the current call?")
- .child("The current window will be closed, and connections to any shared projects will be terminated."
- )
- .primary_action("Leave Call")
- .into_any_element(),
- )
- ],
- )])
- .into_any_element()
+ .children(vec![
+ example_group(vec![single_example(
+ "Basic Alert",
+ AlertModal::new("simple-modal")
+ .title("Do you want to leave the current call?")
+ .child(
+ "The current window will be closed, and connections to any shared projects will be terminated."
+ )
+ .primary_action("Leave Call")
+ .dismiss_label("Cancel")
+ .into_any_element(),
+ )]),
+ example_group(vec![single_example(
+ "Custom Header",
+ AlertModal::new("custom-header-modal")
+ .header(
+ v_flex()
+ .p_3()
+ .bg(cx.theme().colors().background)
+ .gap_1()
+ .child(
+ h_flex()
+ .gap_1()
+ .child(Icon::new(IconName::Warning).color(Color::Warning))
+ .child(Headline::new("Unrecognized Workspace").size(HeadlineSize::Small))
+ )
+ .child(
+ h_flex()
+ .pl(IconSize::default().rems() + rems(0.5))
+ .child(Label::new("~/projects/my-project").color(Color::Muted))
+ )
+ )
+ .child(
+ "Untrusted workspaces are opened in Restricted Mode to protect your system.
+Review .zed/settings.json for any extensions or commands configured by this project.",
+ )
+ .child(
+ v_flex()
+ .mt_1()
+ .child(Label::new("Restricted mode prevents:").color(Color::Muted))
+ .child(ListBulletItem::new("Project settings from being applied"))
+ .child(ListBulletItem::new("Language servers from running"))
+ .child(ListBulletItem::new("MCP integrations from installing"))
+ )
+ .footer(
+ h_flex()
+ .p_3()
+ .justify_between()
+ .child(
+ Checkbox::new("trust-parent", ToggleState::Unselected)
+ .label("Trust all projects in parent directory")
+ )
+ .child(
+ h_flex()
+ .gap_1()
+ .child(Button::new("restricted", "Stay in Restricted Mode").color(Color::Muted))
+ .child(Button::new("trust", "Trust and Continue").style(ButtonStyle::Filled))
+ )
+ )
+ .width(rems(40.))
+ .into_any_element(),
+ )]),
+ ])
+ .into_any_element(),
)
}
}
diff --git a/crates/workspace/src/modal_layer.rs b/crates/workspace/src/modal_layer.rs
index bcd7db3a82aec46405927e118af86cf4a0d4912b..d6f10f703100d89bef5babd4baa590df5fa0c8fd 100644
--- a/crates/workspace/src/modal_layer.rs
+++ b/crates/workspace/src/modal_layer.rs
@@ -171,28 +171,19 @@ impl Render for ModalLayer {
};
div()
- .occlude()
.absolute()
.size_full()
- .top_0()
- .left_0()
- .when(active_modal.modal.fade_out_background(cx), |el| {
+ .inset_0()
+ .occlude()
+ .when(active_modal.modal.fade_out_background(cx), |this| {
let mut background = cx.theme().colors().elevated_surface_background;
background.fade_out(0.2);
- el.bg(background)
+ this.bg(background)
})
- .on_mouse_down(
- MouseButton::Left,
- cx.listener(|this, _, window, cx| {
- this.hide_modal(window, cx);
- }),
- )
.child(
v_flex()
.h(px(0.0))
.top_20()
- .flex()
- .flex_col()
.items_center()
.track_focus(&active_modal.focus_handle)
.child(
diff --git a/crates/workspace/src/security_modal.rs b/crates/workspace/src/security_modal.rs
new file mode 100644
index 0000000000000000000000000000000000000000..f2a94ad81661a2572f35d1d746b04b31fa24f00c
--- /dev/null
+++ b/crates/workspace/src/security_modal.rs
@@ -0,0 +1,373 @@
+//! A UI interface for managing the [`TrustedWorktrees`] data.
+
+use std::{
+ borrow::Cow,
+ path::{Path, PathBuf},
+ sync::Arc,
+};
+
+use collections::{HashMap, HashSet};
+use gpui::{DismissEvent, EventEmitter, FocusHandle, Focusable, WeakEntity};
+
+use project::{
+ WorktreeId,
+ trusted_worktrees::{PathTrust, RemoteHostLocation, TrustedWorktrees},
+ worktree_store::WorktreeStore,
+};
+use smallvec::SmallVec;
+use theme::ActiveTheme;
+use ui::{
+ AlertModal, Checkbox, FluentBuilder, KeyBinding, ListBulletItem, ToggleState, prelude::*,
+};
+
+use crate::{DismissDecision, ModalView, ToggleWorktreeSecurity};
+
+pub struct SecurityModal {
+ restricted_paths: HashMap