diff --git a/crates/git_ui/src/worktree_service.rs b/crates/git_ui/src/worktree_service.rs index 0ec34f3d915..1eda4219092 100644 --- a/crates/git_ui/src/worktree_service.rs +++ b/crates/git_ui/src/worktree_service.rs @@ -252,17 +252,11 @@ fn maybe_propagate_worktree_trust( if ProjectSettings::get_global(cx).session.trust_all_worktrees { return; } - let Some(trusted_store) = TrustedWorktrees::try_get_global(cx) else { - return; - }; - let source_is_trusted = source_workspace .upgrade() .map(|workspace| { let source_worktree_store = workspace.read(cx).project().read(cx).worktree_store(); - !trusted_store - .read(cx) - .has_restricted_worktrees(&source_worktree_store, cx) + !TrustedWorktrees::has_restricted_worktrees(&source_worktree_store, cx) }) .unwrap_or(false); @@ -280,9 +274,11 @@ fn maybe_propagate_worktree_trust( .collect(); if !paths_to_trust.is_empty() { - trusted_store.update(cx, |store, cx| { - store.trust(&worktree_store, paths_to_trust, cx); - }); + if let Some(trusted_store) = TrustedWorktrees::try_get_global(cx) { + trusted_store.update(cx, |store, cx| { + store.trust(&worktree_store, paths_to_trust, cx); + }); + } } }) .ok(); diff --git a/crates/language_tools/src/lsp_button.rs b/crates/language_tools/src/lsp_button.rs index 8b7088dc228..e7c6d5b2160 100644 --- a/crates/language_tools/src/lsp_button.rs +++ b/crates/language_tools/src/lsp_button.rs @@ -13,12 +13,12 @@ use language::language_settings::{EditPredictionProvider, all_language_settings} use client::proto; use collections::HashSet; use editor::{Editor, EditorEvent}; -use gpui::{Anchor, App, Entity, Subscription, Task, TaskExt, WeakEntity, actions}; +use gpui::{Action as _, Anchor, App, Entity, Subscription, Task, TaskExt, WeakEntity, actions}; use language::{BinaryStatus, BufferId, ServerHealth}; use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector}; use project::{ LspStore, LspStoreEvent, Worktree, lsp_store::log_store::GlobalLogStore, - project_settings::ProjectSettings, + project_settings::ProjectSettings, trusted_worktrees::TrustedWorktrees, }; use settings::{Settings as _, SettingsStore}; use ui::{ @@ -26,7 +26,7 @@ use ui::{ }; use util::{ResultExt, paths::PathExt, rel_path::RelPath}; -use workspace::{StatusItemView, Workspace}; +use workspace::{StatusItemView, ToggleWorktreeSecurity, Workspace}; use crate::lsp_log_view; @@ -221,6 +221,45 @@ impl LanguageServerState { return menu; }; + let is_restricted = self + .workspace + .upgrade() + .map(|workspace| { + let worktree_store = workspace.read(cx).project().read(cx).worktree_store(); + TrustedWorktrees::has_restricted_worktrees(&worktree_store, cx) + }) + .unwrap_or(false); + + if is_restricted { + menu = menu.custom_entry( + move |_window, _cx| { + v_flex() + .child( + h_flex() + .gap_1() + .child( + Icon::new(IconName::Warning) + .color(Color::Warning) + .size(IconSize::XSmall), + ) + .child( + Label::new("Project is in Restricted Mode") + .size(LabelSize::Small), + ), + ) + .child( + Label::new("Language Servers can't run until you trust this project.") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .into_any_element() + }, + move |window, cx| { + window.dispatch_action(ToggleWorktreeSecurity.boxed_clone(), cx); + }, + ); + } + let server_metadata = self .lsp_store .update(cx, |lsp_store, _| { @@ -832,12 +871,18 @@ impl LspButton { lsp_menu_refresh: Task::ready(()), _subscriptions: vec![settings_subscription, lsp_store_subscription], }; - if !lsp_button - .server_state - .read(cx) - .language_servers - .binary_statuses - .is_empty() + let is_restricted = TrustedWorktrees::has_restricted_worktrees( + &workspace.project().read(cx).worktree_store(), + cx, + ); + + if is_restricted + || !lsp_button + .server_state + .read(cx) + .language_servers + .binary_statuses + .is_empty() { lsp_button.refresh_lsp_menu(true, window, cx); } @@ -1258,7 +1303,20 @@ impl StatusItemView for LspButton { impl Render for LspButton { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl ui::IntoElement { - if self.server_state.read(cx).language_servers.is_empty() || self.lsp_menu.is_none() { + let is_restricted = self + .server_state + .read(cx) + .workspace + .upgrade() + .map(|workspace| { + let worktree_store = workspace.read(cx).project().read(cx).worktree_store(); + TrustedWorktrees::has_restricted_worktrees(&worktree_store, cx) + }) + .unwrap_or(false); + + if !is_restricted + && (self.server_state.read(cx).language_servers.is_empty() || self.lsp_menu.is_none()) + { return div().hidden(); } @@ -1288,7 +1346,12 @@ impl Render for LspButton { } } - let (indicator, description) = if has_errors { + let (indicator, description) = if is_restricted { + ( + Some(Indicator::dot().color(Color::Warning)), + "Restricted Mode", + ) + } else if has_errors { ( Some(Indicator::dot().color(Color::Error)), "Server with errors", @@ -1333,6 +1396,7 @@ impl Render for LspButton { IconButton::new("zed-lsp-tool-button", IconName::BoltOutlined) .when_some(indicator, IconButton::indicator) .icon_size(IconSize::Small) + .when(is_restricted, |s| s.icon_color(Color::Warning)) .indicator_border_color(Some(cx.theme().colors().status_bar_background)), move |_window, cx| { Tooltip::with_meta("Language Servers", Some(&ToggleMenu), description, cx) diff --git a/crates/project/src/trusted_worktrees.rs b/crates/project/src/trusted_worktrees.rs index 69d410adc66..8d8804c3f97 100644 --- a/crates/project/src/trusted_worktrees.rs +++ b/crates/project/src/trusted_worktrees.rs @@ -113,6 +113,17 @@ impl TrustedWorktrees { pub fn try_get_global(cx: &App) -> Option> { cx.try_global::().map(|this| this.0.clone()) } + + /// Whether the given project store has any restricted worktrees. + pub fn has_restricted_worktrees(worktree_store: &Entity, cx: &App) -> bool { + Self::try_get_global(cx) + .map(|trusted| { + trusted + .read(cx) + .has_restricted_worktrees(worktree_store, cx) + }) + .unwrap_or(false) + } } /// A collection of worktrees that are considered trusted and not trusted. diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index f8d938e9eec..02bbacdfa30 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -3350,6 +3350,65 @@ impl SettingsWindow { .into_any_element() } + let mut restricted_banner = gpui::Empty.into_any_element(); + if let SettingsUiFile::Project((worktree_id, _)) = &self.current_file { + let worktree_id = *worktree_id; + let is_restricted = all_projects(self.original_window.as_ref(), cx) + .find(|project| project.read(cx).worktree_for_id(worktree_id, cx).is_some()) + .map(|project| { + let worktree_store = project.read(cx).worktree_store(); + project::trusted_worktrees::TrustedWorktrees::has_restricted_worktrees( + &worktree_store, + cx, + ) + }) + .unwrap_or(false); + + if is_restricted { + let original_window = self.original_window; + restricted_banner = Banner::new() + .severity(Severity::Warning) + .child( + v_flex() + .my_0p5() + .gap_0p5() + .child(Label::new("Restricted Mode")) + .child( + Label::new( + "This project is in restricted mode. Some project settings may not apply.", + ) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + .action_slot( + div().pr_2().pb_1().child( + Button::new("manage-trust", "Manage Trust") + .style(ButtonStyle::Tinted(ui::TintColor::Warning)) + .on_click(cx.listener(move |_this, _, window, cx| { + if let Some(original_window) = original_window { + original_window + .update(cx, |multi_workspace, window, cx| { + multi_workspace + .workspace() + .update(cx, |workspace, cx| { + workspace + .show_worktree_trust_security_modal( + true, window, cx, + ); + }); + }) + .log_err(); + } + // Close the settings window + window.remove_window(); + })), + ), + ) + .into_any_element(); + } + } + v_flex() .id("settings-ui-page") .on_action(cx.listener(|this, _: &menu::SelectNext, window, cx| { @@ -3440,7 +3499,8 @@ impl SettingsWindow { .px_8() .gap_2() .child(page_header) - .child(warning_banner), + .child(warning_banner) + .child(restricted_banner), ) .child( div() diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index c15f840e69d..3bc12a20748 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -641,13 +641,8 @@ 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); + let has_restricted_worktrees = + TrustedWorktrees::has_restricted_worktrees(&self.project.read(cx).worktree_store(), cx); if !has_restricted_worktrees { return None; } diff --git a/crates/workspace/src/security_modal.rs b/crates/workspace/src/security_modal.rs index 2130a1d1eca..89ce2abfd66 100644 --- a/crates/workspace/src/security_modal.rs +++ b/crates/workspace/src/security_modal.rs @@ -56,11 +56,17 @@ impl ModalView for SecurityModal { fn on_before_dismiss(&mut self, _: &mut Window, _: &mut Context) -> DismissDecision { match self.trusted { - Some(false) => telemetry::event!("Open in Restricted", source = "Worktree Trust Modal"), - Some(true) => telemetry::event!("Trust and Continue", source = "Worktree Trust Modal"), - None => telemetry::event!("Dismissed", source = "Worktree Trust Modal"), + Some(false) => { + telemetry::event!("Open in Restricted", source = "Worktree Trust Modal"); + DismissDecision::Dismiss(true) + } + Some(true) => { + telemetry::event!("Trust and Continue", source = "Worktree Trust Modal"); + DismissDecision::Dismiss(true) + } + // Block dismiss via escape or clicking outside; user must pick an action + None => DismissDecision::Dismiss(false), } - DismissDecision::Dismiss(true) } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index da8ffe972ee..599a2d23681 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2122,6 +2122,15 @@ impl Workspace { .log_err(); } + // Auto-show the security modal if the project has restricted worktrees + window + .update(cx, |_, window, cx| { + workspace.update(cx, |workspace, cx| { + workspace.show_worktree_trust_security_modal(false, window, cx); + }); + }) + .log_err(); + Ok(OpenResult { window, workspace, @@ -8014,13 +8023,10 @@ impl Workspace { }); } } else { - 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); + let has_restricted_worktrees = TrustedWorktrees::has_restricted_worktrees( + &self.project().read(cx).worktree_store(), + cx, + ); if has_restricted_worktrees { let project = self.project().read(cx); let remote_host = project