mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Make restricted mode more obvious (#57056)
Closes TRA-150 This PR makes the restricted mode more obvious by: - Immediately opening the restricted mode modal upon opening an untrusted project - Disabling dismissing the modal on escape or click away to force choosing one of the two options (and avoid accidentally staying in restricted mode by simply dismissing it) - Showing the LSP button but with communication about language servers being disabled for untrusted projects - Showing a banner in the project settings with the same communication The motivation for this change was that we tried to be minimal with how we communicate a project is untrusted, but it was so minimal that people were confused as to why language servers and other settings weren't working. It was easy to miss the title bar button, for some reason. The changes in this PR makes it so acting on this decision (trust or not a project) is mandatory in order to even start to interact with the project. I appreciate changes here are more aggressive, but I think it's better to make you think about this decision vs. letting you be confused as to why you don't see LS completions or formatting. Release Notes: - Made restricted mode more obvious, demanding immediate action when opening an untrusted project.
This commit is contained in:
parent
ea01b926ea
commit
ec9ba5f069
7 changed files with 178 additions and 40 deletions
|
|
@ -252,17 +252,11 @@ fn maybe_propagate_worktree_trust(
|
||||||
if ProjectSettings::get_global(cx).session.trust_all_worktrees {
|
if ProjectSettings::get_global(cx).session.trust_all_worktrees {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(trusted_store) = TrustedWorktrees::try_get_global(cx) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let source_is_trusted = source_workspace
|
let source_is_trusted = source_workspace
|
||||||
.upgrade()
|
.upgrade()
|
||||||
.map(|workspace| {
|
.map(|workspace| {
|
||||||
let source_worktree_store = workspace.read(cx).project().read(cx).worktree_store();
|
let source_worktree_store = workspace.read(cx).project().read(cx).worktree_store();
|
||||||
!trusted_store
|
!TrustedWorktrees::has_restricted_worktrees(&source_worktree_store, cx)
|
||||||
.read(cx)
|
|
||||||
.has_restricted_worktrees(&source_worktree_store, cx)
|
|
||||||
})
|
})
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
|
@ -280,9 +274,11 @@ fn maybe_propagate_worktree_trust(
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if !paths_to_trust.is_empty() {
|
if !paths_to_trust.is_empty() {
|
||||||
trusted_store.update(cx, |store, cx| {
|
if let Some(trusted_store) = TrustedWorktrees::try_get_global(cx) {
|
||||||
store.trust(&worktree_store, paths_to_trust, cx);
|
trusted_store.update(cx, |store, cx| {
|
||||||
});
|
store.trust(&worktree_store, paths_to_trust, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,12 @@ use language::language_settings::{EditPredictionProvider, all_language_settings}
|
||||||
use client::proto;
|
use client::proto;
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use editor::{Editor, EditorEvent};
|
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 language::{BinaryStatus, BufferId, ServerHealth};
|
||||||
use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector};
|
use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector};
|
||||||
use project::{
|
use project::{
|
||||||
LspStore, LspStoreEvent, Worktree, lsp_store::log_store::GlobalLogStore,
|
LspStore, LspStoreEvent, Worktree, lsp_store::log_store::GlobalLogStore,
|
||||||
project_settings::ProjectSettings,
|
project_settings::ProjectSettings, trusted_worktrees::TrustedWorktrees,
|
||||||
};
|
};
|
||||||
use settings::{Settings as _, SettingsStore};
|
use settings::{Settings as _, SettingsStore};
|
||||||
use ui::{
|
use ui::{
|
||||||
|
|
@ -26,7 +26,7 @@ use ui::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use util::{ResultExt, paths::PathExt, rel_path::RelPath};
|
use util::{ResultExt, paths::PathExt, rel_path::RelPath};
|
||||||
use workspace::{StatusItemView, Workspace};
|
use workspace::{StatusItemView, ToggleWorktreeSecurity, Workspace};
|
||||||
|
|
||||||
use crate::lsp_log_view;
|
use crate::lsp_log_view;
|
||||||
|
|
||||||
|
|
@ -221,6 +221,45 @@ impl LanguageServerState {
|
||||||
return menu;
|
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
|
let server_metadata = self
|
||||||
.lsp_store
|
.lsp_store
|
||||||
.update(cx, |lsp_store, _| {
|
.update(cx, |lsp_store, _| {
|
||||||
|
|
@ -832,12 +871,18 @@ impl LspButton {
|
||||||
lsp_menu_refresh: Task::ready(()),
|
lsp_menu_refresh: Task::ready(()),
|
||||||
_subscriptions: vec![settings_subscription, lsp_store_subscription],
|
_subscriptions: vec![settings_subscription, lsp_store_subscription],
|
||||||
};
|
};
|
||||||
if !lsp_button
|
let is_restricted = TrustedWorktrees::has_restricted_worktrees(
|
||||||
.server_state
|
&workspace.project().read(cx).worktree_store(),
|
||||||
.read(cx)
|
cx,
|
||||||
.language_servers
|
);
|
||||||
.binary_statuses
|
|
||||||
.is_empty()
|
if is_restricted
|
||||||
|
|| !lsp_button
|
||||||
|
.server_state
|
||||||
|
.read(cx)
|
||||||
|
.language_servers
|
||||||
|
.binary_statuses
|
||||||
|
.is_empty()
|
||||||
{
|
{
|
||||||
lsp_button.refresh_lsp_menu(true, window, cx);
|
lsp_button.refresh_lsp_menu(true, window, cx);
|
||||||
}
|
}
|
||||||
|
|
@ -1258,7 +1303,20 @@ impl StatusItemView for LspButton {
|
||||||
|
|
||||||
impl Render for LspButton {
|
impl Render for LspButton {
|
||||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> 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();
|
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)),
|
Some(Indicator::dot().color(Color::Error)),
|
||||||
"Server with errors",
|
"Server with errors",
|
||||||
|
|
@ -1333,6 +1396,7 @@ impl Render for LspButton {
|
||||||
IconButton::new("zed-lsp-tool-button", IconName::BoltOutlined)
|
IconButton::new("zed-lsp-tool-button", IconName::BoltOutlined)
|
||||||
.when_some(indicator, IconButton::indicator)
|
.when_some(indicator, IconButton::indicator)
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
|
.when(is_restricted, |s| s.icon_color(Color::Warning))
|
||||||
.indicator_border_color(Some(cx.theme().colors().status_bar_background)),
|
.indicator_border_color(Some(cx.theme().colors().status_bar_background)),
|
||||||
move |_window, cx| {
|
move |_window, cx| {
|
||||||
Tooltip::with_meta("Language Servers", Some(&ToggleMenu), description, cx)
|
Tooltip::with_meta("Language Servers", Some(&ToggleMenu), description, cx)
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,17 @@ impl TrustedWorktrees {
|
||||||
pub fn try_get_global(cx: &App) -> Option<Entity<TrustedWorktreesStore>> {
|
pub fn try_get_global(cx: &App) -> Option<Entity<TrustedWorktreesStore>> {
|
||||||
cx.try_global::<Self>().map(|this| this.0.clone())
|
cx.try_global::<Self>().map(|this| this.0.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the given project store has any restricted worktrees.
|
||||||
|
pub fn has_restricted_worktrees(worktree_store: &Entity<WorktreeStore>, 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.
|
/// A collection of worktrees that are considered trusted and not trusted.
|
||||||
|
|
|
||||||
|
|
@ -3350,6 +3350,65 @@ impl SettingsWindow {
|
||||||
.into_any_element()
|
.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()
|
v_flex()
|
||||||
.id("settings-ui-page")
|
.id("settings-ui-page")
|
||||||
.on_action(cx.listener(|this, _: &menu::SelectNext, window, cx| {
|
.on_action(cx.listener(|this, _: &menu::SelectNext, window, cx| {
|
||||||
|
|
@ -3440,7 +3499,8 @@ impl SettingsWindow {
|
||||||
.px_8()
|
.px_8()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(page_header)
|
.child(page_header)
|
||||||
.child(warning_banner),
|
.child(warning_banner)
|
||||||
|
.child(restricted_banner),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
|
|
||||||
|
|
@ -641,13 +641,8 @@ impl TitleBar {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_restricted_mode(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
pub fn render_restricted_mode(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||||
let has_restricted_worktrees = TrustedWorktrees::try_get_global(cx)
|
let has_restricted_worktrees =
|
||||||
.map(|trusted_worktrees| {
|
TrustedWorktrees::has_restricted_worktrees(&self.project.read(cx).worktree_store(), cx);
|
||||||
trusted_worktrees
|
|
||||||
.read(cx)
|
|
||||||
.has_restricted_worktrees(&self.project.read(cx).worktree_store(), cx)
|
|
||||||
})
|
|
||||||
.unwrap_or(false);
|
|
||||||
if !has_restricted_worktrees {
|
if !has_restricted_worktrees {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,11 +56,17 @@ impl ModalView for SecurityModal {
|
||||||
|
|
||||||
fn on_before_dismiss(&mut self, _: &mut Window, _: &mut Context<Self>) -> DismissDecision {
|
fn on_before_dismiss(&mut self, _: &mut Window, _: &mut Context<Self>) -> DismissDecision {
|
||||||
match self.trusted {
|
match self.trusted {
|
||||||
Some(false) => telemetry::event!("Open in Restricted", source = "Worktree Trust Modal"),
|
Some(false) => {
|
||||||
Some(true) => telemetry::event!("Trust and Continue", source = "Worktree Trust Modal"),
|
telemetry::event!("Open in Restricted", source = "Worktree Trust Modal");
|
||||||
None => telemetry::event!("Dismissed", 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2122,6 +2122,15 @@ impl Workspace {
|
||||||
.log_err();
|
.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 {
|
Ok(OpenResult {
|
||||||
window,
|
window,
|
||||||
workspace,
|
workspace,
|
||||||
|
|
@ -8014,13 +8023,10 @@ impl Workspace {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let has_restricted_worktrees = TrustedWorktrees::try_get_global(cx)
|
let has_restricted_worktrees = TrustedWorktrees::has_restricted_worktrees(
|
||||||
.map(|trusted_worktrees| {
|
&self.project().read(cx).worktree_store(),
|
||||||
trusted_worktrees
|
cx,
|
||||||
.read(cx)
|
);
|
||||||
.has_restricted_worktrees(&self.project().read(cx).worktree_store(), cx)
|
|
||||||
})
|
|
||||||
.unwrap_or(false);
|
|
||||||
if has_restricted_worktrees {
|
if has_restricted_worktrees {
|
||||||
let project = self.project().read(cx);
|
let project = self.project().read(cx);
|
||||||
let remote_host = project
|
let remote_host = project
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue