Compare commits

...

12 Commits

Author SHA1 Message Date
Matt Miller
f34d12dd27 SecurityModal uses updated AlertModal 2025-12-04 18:03:27 -06:00
Kirill Bulatov
57b880526a Add initial functioning design of the new security modal 2025-12-04 22:34:53 +02:00
Kirill Bulatov
039fe76a84 Show a rudimental security modal
Rework new tasks
2025-12-04 20:28:48 +02:00
Kirill Bulatov
be7bfa1803 Add restricted mode indicator to title bar 2025-12-04 20:28:48 +02:00
Kirill Bulatov
792641796a Show untrusted worktrees notifications in proper workspaces 2025-12-04 20:28:48 +02:00
Kirill Bulatov
b14b869aa6 Store untrusted worktrees globally 2025-12-04 20:28:48 +02:00
Kirill Bulatov
9473e69fff Apply a review suggestion 2025-12-04 20:28:48 +02:00
Kirill Bulatov
26945eea1c Fix most of the TODOs 2025-12-04 20:28:48 +02:00
Kirill Bulatov
7cf39ed7e5 Only check trusted worktrees on local settings sync 2025-12-04 20:28:48 +02:00
Kirill Bulatov
2ddd3a033f Small fixes 2025-12-04 20:28:48 +02:00
Kirill Bulatov
ddb7eb1747 Show trust notifications 2025-12-04 20:28:48 +02:00
Kirill Bulatov
db75a2c62a Implement initial project settings trust mechanism 2025-12-04 20:28:48 +02:00
16 changed files with 931 additions and 98 deletions

3
Cargo.lock generated
View File

@@ -13076,6 +13076,7 @@ dependencies = [
"semver", "semver",
"serde", "serde",
"serde_json", "serde_json",
"session",
"settings", "settings",
"sha2", "sha2",
"shellexpand 2.1.2", "shellexpand 2.1.2",
@@ -15383,6 +15384,7 @@ dependencies = [
name = "session" name = "session"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"collections",
"db", "db",
"gpui", "gpui",
"serde_json", "serde_json",
@@ -17509,6 +17511,7 @@ dependencies = [
"rpc", "rpc",
"schemars", "schemars",
"serde", "serde",
"session",
"settings", "settings",
"smallvec", "smallvec",
"story", "story",

View File

@@ -2040,7 +2040,11 @@
// dirty files when closing the application. // dirty files when closing the application.
// //
// Default: true // Default: true
"restore_unsaved_buffers": true "restore_unsaved_buffers": true,
// Whether or not to skip project trust checks and synchronize project settings from any worktree automatically.
//
// Default: false
"trust_all_worktrees": false
}, },
// Zed's Prettier integration settings. // Zed's Prettier integration settings.
// Allows to enable/disable formatting with Prettier // Allows to enable/disable formatting with Prettier

View File

@@ -70,6 +70,7 @@ schemars.workspace = true
semver.workspace = true semver.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
session.workspace = true
settings.workspace = true settings.workspace = true
sha2.workspace = true sha2.workspace = true
shellexpand.workspace = true shellexpand.workspace = true

View File

@@ -17,13 +17,14 @@ use rpc::{
}; };
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use session::TrustedWorktreesStorage;
pub use settings::DirenvSettings; pub use settings::DirenvSettings;
pub use settings::LspSettings; pub use settings::LspSettings;
use settings::{ use settings::{
DapSettingsContent, InvalidSettingsError, LocalSettingsKind, RegisterSetting, Settings, DapSettingsContent, InvalidSettingsError, LocalSettingsKind, RegisterSetting, Settings,
SettingsLocation, SettingsStore, parse_json_with_comments, watch_config_file, SettingsLocation, SettingsStore, parse_json_with_comments, watch_config_file,
}; };
use std::{path::PathBuf, sync::Arc, time::Duration}; use std::{cell::OnceCell, collections::BTreeMap, path::PathBuf, sync::Arc, time::Duration};
use task::{DebugTaskFile, TaskTemplates, VsCodeDebugTaskFile, VsCodeTaskFile}; use task::{DebugTaskFile, TaskTemplates, VsCodeDebugTaskFile, VsCodeTaskFile};
use util::{ResultExt, rel_path::RelPath, serde::default_true}; use util::{ResultExt, rel_path::RelPath, serde::default_true};
use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId}; use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
@@ -83,6 +84,10 @@ pub struct SessionSettings {
/// ///
/// Default: true /// Default: true
pub restore_unsaved_buffers: bool, pub restore_unsaved_buffers: bool,
/// Whether or not to skip project trust checks and synchronize project settings from any worktree automatically.
///
/// Default: false
pub trust_all_worktrees: bool,
} }
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
@@ -570,6 +575,7 @@ impl Settings for ProjectSettings {
load_direnv: project.load_direnv.clone().unwrap(), load_direnv: project.load_direnv.clone().unwrap(),
session: SessionSettings { session: SessionSettings {
restore_unsaved_buffers: content.session.unwrap().restore_unsaved_buffers.unwrap(), restore_unsaved_buffers: content.session.unwrap().restore_unsaved_buffers.unwrap(),
trust_all_worktrees: content.session.unwrap().trust_all_worktrees.unwrap(),
}, },
} }
} }
@@ -595,6 +601,8 @@ pub struct SettingsObserver {
worktree_store: Entity<WorktreeStore>, worktree_store: Entity<WorktreeStore>,
project_id: u64, project_id: u64,
task_store: Entity<TaskStore>, task_store: Entity<TaskStore>,
pending_local_settings: HashMap<PathBuf, BTreeMap<(WorktreeId, Arc<RelPath>), Option<String>>>,
_trusted_worktrees_watcher: Option<Subscription>,
_user_settings_watcher: Option<Subscription>, _user_settings_watcher: Option<Subscription>,
_global_task_config_watcher: Task<()>, _global_task_config_watcher: Task<()>,
_global_debug_config_watcher: Task<()>, _global_debug_config_watcher: Task<()>,
@@ -620,11 +628,65 @@ impl SettingsObserver {
cx.subscribe(&worktree_store, Self::on_worktree_store_event) cx.subscribe(&worktree_store, Self::on_worktree_store_event)
.detach(); .detach();
let weak_settings_observer = cx.weak_entity();
let _trusted_worktrees_watcher = if cx.has_global::<TrustedWorktreesStorage>() {
cx.update_global::<TrustedWorktreesStorage, _>(|trusted_worktrees, cx| {
let watcher = trusted_worktrees.subscribe(cx, move |_, e, cx| match e {
session::Event::TrustedWorktree(trusted_path) => {
weak_settings_observer
.update(cx, |settings_observer, cx| {
if let Some(pending_local_settings) = settings_observer
.pending_local_settings
.remove(trusted_path)
{
for ((worktree_id, directory_path), settings_contents) in
pending_local_settings
{
apply_local_settings(
worktree_id,
&directory_path,
LocalSettingsKind::Settings,
&settings_contents,
cx,
);
if let Some(downstream_client) =
&settings_observer.downstream_client
{
downstream_client
.send(proto::UpdateWorktreeSettings {
project_id: settings_observer.project_id,
worktree_id: worktree_id.to_proto(),
path: directory_path.to_proto(),
content: settings_contents,
kind: Some(
local_settings_kind_to_proto(
LocalSettingsKind::Settings,
)
.into(),
),
})
.log_err();
}
}
}
})
.ok();
}
session::Event::UntrustedWorktree(_) => {}
});
Some(watcher)
})
} else {
None
};
Self { Self {
worktree_store, worktree_store,
task_store, task_store,
mode: SettingsObserverMode::Local(fs.clone()), mode: SettingsObserverMode::Local(fs.clone()),
downstream_client: None, downstream_client: None,
_trusted_worktrees_watcher,
pending_local_settings: HashMap::default(),
_user_settings_watcher: None, _user_settings_watcher: None,
project_id: REMOTE_SERVER_PROJECT_ID, project_id: REMOTE_SERVER_PROJECT_ID,
_global_task_config_watcher: Self::subscribe_to_global_task_file_changes( _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
@@ -677,6 +739,8 @@ impl SettingsObserver {
mode: SettingsObserverMode::Remote, mode: SettingsObserverMode::Remote,
downstream_client: None, downstream_client: None,
project_id: REMOTE_SERVER_PROJECT_ID, project_id: REMOTE_SERVER_PROJECT_ID,
_trusted_worktrees_watcher: None,
pending_local_settings: HashMap::default(),
_user_settings_watcher: user_settings_watcher, _user_settings_watcher: user_settings_watcher,
_global_task_config_watcher: Self::subscribe_to_global_task_file_changes( _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
fs.clone(), fs.clone(),
@@ -968,36 +1032,36 @@ impl SettingsObserver {
let worktree_id = worktree.read(cx).id(); let worktree_id = worktree.read(cx).id();
let remote_worktree_id = worktree.read(cx).id(); let remote_worktree_id = worktree.read(cx).id();
let task_store = self.task_store.clone(); let task_store = self.task_store.clone();
let worktree_abs_path = worktree.read(cx).abs_path();
let can_trust_worktree = OnceCell::new();
for (directory, kind, file_content) in settings_contents { for (directory, kind, file_content) in settings_contents {
let mut applied = true;
match kind { match kind {
LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx LocalSettingsKind::Settings => {
.update_global::<SettingsStore, _>(|store, cx| { if *can_trust_worktree.get_or_init(|| {
let result = store.set_local_settings( if cx.has_global::<TrustedWorktreesStorage>() {
worktree_id, cx.update_global::<TrustedWorktreesStorage, _>(
directory.clone(), |trusted_worktrees_storage, cx| {
kind, trusted_worktrees_storage
file_content.as_deref(), .can_trust_path(worktree_abs_path.as_ref(), cx)
cx, },
); )
} else {
match result { true
Err(InvalidSettingsError::LocalSettings { path, message }) => {
log::error!("Failed to set local settings in {path:?}: {message}");
cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
InvalidSettingsError::LocalSettings { path, message },
)));
}
Err(e) => {
log::error!("Failed to set local settings: {e}");
}
Ok(()) => {
cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(directory
.as_std_path()
.join(local_settings_file_relative_path().as_std_path()))));
}
} }
}), }) {
apply_local_settings(worktree_id, &directory, kind, &file_content, cx)
} else {
applied = false;
self.pending_local_settings
.entry(worktree_abs_path.to_path_buf())
.or_default()
.insert((worktree_id, directory.clone()), file_content.clone());
}
}
LocalSettingsKind::Editorconfig => {
apply_local_settings(worktree_id, &directory, kind, &file_content, cx)
}
LocalSettingsKind::Tasks => { LocalSettingsKind::Tasks => {
let result = task_store.update(cx, |task_store, cx| { let result = task_store.update(cx, |task_store, cx| {
task_store.update_user_tasks( task_store.update_user_tasks(
@@ -1060,16 +1124,18 @@ impl SettingsObserver {
} }
}; };
if let Some(downstream_client) = &self.downstream_client { if applied {
downstream_client if let Some(downstream_client) = &self.downstream_client {
.send(proto::UpdateWorktreeSettings { downstream_client
project_id: self.project_id, .send(proto::UpdateWorktreeSettings {
worktree_id: remote_worktree_id.to_proto(), project_id: self.project_id,
path: directory.to_proto(), worktree_id: remote_worktree_id.to_proto(),
content: file_content.clone(), path: directory.to_proto(),
kind: Some(local_settings_kind_to_proto(kind).into()), content: file_content.clone(),
}) kind: Some(local_settings_kind_to_proto(kind).into()),
.log_err(); })
.log_err();
}
} }
} }
} }
@@ -1186,6 +1252,41 @@ impl SettingsObserver {
} }
} }
fn apply_local_settings(
worktree_id: WorktreeId,
directory: &Arc<RelPath>,
kind: LocalSettingsKind,
file_content: &Option<String>,
cx: &mut Context<'_, SettingsObserver>,
) {
cx.update_global::<SettingsStore, _>(|store, cx| {
let result = store.set_local_settings(
worktree_id,
directory.clone(),
kind,
file_content.as_deref(),
cx,
);
match result {
Err(InvalidSettingsError::LocalSettings { path, message }) => {
log::error!("Failed to set local settings in {path:?}: {message}");
cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
InvalidSettingsError::LocalSettings { path, message },
)));
}
Err(e) => {
log::error!("Failed to set local settings: {e}");
}
Ok(()) => {
cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(directory
.as_std_path()
.join(local_settings_file_relative_path().as_std_path()))));
}
}
})
}
pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind { pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
match kind { match kind {
proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings, proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,

View File

@@ -18,6 +18,7 @@ test-support = [
] ]
[dependencies] [dependencies]
collections.workspace = true
db.workspace = true db.workspace = true
gpui.workspace = true gpui.workspace = true
uuid.workspace = true uuid.workspace = true

View File

@@ -1,9 +1,31 @@
use std::time::Duration; use std::{
path::{Path, PathBuf},
time::Duration,
};
use collections::HashSet;
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use gpui::{App, AppContext as _, Context, Subscription, Task, WindowId}; use gpui::{
App, AppContext as _, Context, Entity, EventEmitter, Global, Subscription, Task, Window,
WindowId,
};
use util::ResultExt; use util::ResultExt;
pub fn init(cx: &mut App) {
cx.spawn(async move |cx| {
let trusted_worktrees = TrustedWorktrees::new().await;
cx.update(|cx| {
let trusted_worktees_storage = TrustedWorktreesStorage {
trusted: cx.new(|_| trusted_worktrees),
untrusted: HashSet::default(),
};
cx.set_global(trusted_worktees_storage);
})
.log_err();
})
.detach();
}
pub struct Session { pub struct Session {
session_id: String, session_id: String,
old_session_id: Option<String>, old_session_id: Option<String>,
@@ -12,6 +34,8 @@ pub struct Session {
const SESSION_ID_KEY: &str = "session_id"; const SESSION_ID_KEY: &str = "session_id";
const SESSION_WINDOW_STACK_KEY: &str = "session_window_stack"; const SESSION_WINDOW_STACK_KEY: &str = "session_window_stack";
const TRUSTED_WORKSPACES_KEY: &str = "trusted_workspaces";
const TRUSTED_WORKSPACES_SEPARATOR: &str = "<|>";
impl Session { impl Session {
pub async fn new(session_id: String) -> Self { pub async fn new(session_id: String) -> Self {
@@ -108,6 +132,177 @@ impl AppSession {
} }
} }
// TODO kb move the whole trusted shebang into `project` crate.
/// A collection of worktree absolute paths that are considered trusted.
/// This can be used when checking for this criteria before enabling certain features.
#[derive(Clone)]
pub struct TrustedWorktreesStorage {
trusted: Entity<TrustedWorktrees>,
untrusted: HashSet<PathBuf>,
}
#[derive(Debug)]
pub enum Event {
TrustedWorktree(PathBuf),
UntrustedWorktree(PathBuf),
}
/// A collection of absolute paths for trusted worktrees.
/// Such worktrees' local settings will be processed and applied.
///
/// Emits an event each time the worktree path checked and found not trusted,
/// or a certain worktree path had been trusted.
struct TrustedWorktrees {
worktree_roots: HashSet<PathBuf>,
serialization_task: Task<()>,
}
impl EventEmitter<Event> for TrustedWorktrees {}
impl TrustedWorktrees {
async fn new() -> Self {
Self {
worktree_roots: KEY_VALUE_STORE
.read_kvp(TRUSTED_WORKSPACES_KEY)
.ok()
.flatten()
.map(|workspaces| {
workspaces
.split(TRUSTED_WORKSPACES_SEPARATOR)
.map(|workspace_path| PathBuf::from(workspace_path))
.collect()
})
.unwrap_or_default(),
serialization_task: Task::ready(()),
}
}
fn trust_path(&mut self, abs_path: PathBuf, cx: &mut Context<'_, Self>) {
debug_assert!(
abs_path.is_absolute(),
"Cannot trust non-absolute path {abs_path:?}"
);
let updated = self.worktree_roots.insert(abs_path.clone());
if updated {
let new_worktree_roots =
self.worktree_roots
.iter()
.fold(String::new(), |mut acc, path| {
if !acc.is_empty() {
acc.push_str(TRUSTED_WORKSPACES_SEPARATOR);
}
acc.push_str(&path.to_string_lossy());
acc
});
self.serialization_task = cx.background_spawn(async move {
KEY_VALUE_STORE
.write_kvp(TRUSTED_WORKSPACES_KEY.to_string(), new_worktree_roots)
.await
.log_err();
});
// TODO kb wrong: need to emut multiple worktrees, as we can trust some high-level directory
cx.emit(Event::TrustedWorktree(abs_path));
}
}
fn clear(&mut self, cx: &App) {
self.worktree_roots.clear();
self.serialization_task = cx.background_spawn(async move {
KEY_VALUE_STORE
.write_kvp(TRUSTED_WORKSPACES_KEY.to_string(), String::new())
.await
.log_err();
});
}
}
impl Global for TrustedWorktreesStorage {}
impl TrustedWorktreesStorage {
pub fn subscribe<T: 'static>(
&self,
cx: &mut Context<T>,
mut on_event: impl FnMut(&mut T, &Event, &mut Context<T>) + 'static,
) -> Subscription {
cx.subscribe(&self.trusted, move |t, _, e, cx| on_event(t, e, cx))
}
pub fn subscribe_in<T: 'static>(
&self,
window: &mut Window,
cx: &mut Context<T>,
mut on_event: impl FnMut(&mut T, &Event, &mut Window, &mut Context<T>) + 'static,
) -> Subscription {
cx.subscribe_in(&self.trusted, window, move |t, _, e, window, cx| {
on_event(t, e, window, cx)
})
}
/// Adds a worktree absolute path to the trusted list.
/// This will emit [`Event::TrustedWorktree`] event.
pub fn trust_path(&mut self, abs_path: PathBuf, cx: &mut App) {
self.untrusted.remove(&abs_path);
self.trusted.update(cx, |trusted_worktrees, cx| {
trusted_worktrees.trust_path(abs_path, cx)
});
}
/// Checks whether a certain worktree absolute path is trusted.
/// If not, emits [`Event::UntrustedWorktree`] event.
pub fn can_trust_path(&mut self, abs_path: &Path, cx: &mut App) -> bool {
debug_assert!(
abs_path.is_absolute(),
"Cannot check if trusting non-absolute path {abs_path:?}"
);
self.trusted.update(cx, |trusted_worktrees, cx| {
let trusted_worktree_roots = &trusted_worktrees.worktree_roots;
let mut can_trust = !self.untrusted.contains(abs_path);
if can_trust {
can_trust = if trusted_worktree_roots.len() > 100 {
let mut path = Some(abs_path);
while let Some(path_to_check) = path {
if trusted_worktree_roots.contains(path_to_check) {
return true;
}
path = path_to_check.parent();
}
false
} else {
trusted_worktree_roots
.iter()
.any(|trusted_root| abs_path.starts_with(&trusted_root))
};
}
if !can_trust {
if self.untrusted.insert(abs_path.to_owned()) {
cx.emit(Event::UntrustedWorktree(abs_path.to_owned()));
}
}
can_trust
})
}
pub fn untrusted_worktrees(&self) -> &HashSet<PathBuf> {
&self.untrusted
}
pub fn trust_all(&mut self, cx: &mut App) {
for untrusted_path in std::mem::take(&mut self.untrusted) {
self.trust_path(untrusted_path, cx);
}
}
pub fn clear_trusted_paths(&self, cx: &mut App) {
self.trusted.update(cx, |trusted_worktrees, cx| {
trusted_worktrees.clear(cx);
});
}
}
fn window_stack(cx: &App) -> Option<Vec<u64>> { fn window_stack(cx: &App) -> Option<Vec<u64>> {
Some( Some(
cx.window_stack()? cx.window_stack()?

View File

@@ -187,6 +187,10 @@ pub struct SessionSettingsContent {
/// ///
/// Default: true /// Default: true
pub restore_unsaved_buffers: Option<bool>, pub restore_unsaved_buffers: Option<bool>,
/// Whether or not to skip project trust checks and synchronize project settings from any worktree automatically.
///
/// Default: false
pub trust_all_worktrees: Option<bool>,
} }
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, MergeFrom, Debug)] #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, MergeFrom, Debug)]

View File

@@ -138,6 +138,28 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
metadata: None, metadata: None,
files: USER, files: USER,
}), }),
SettingsPageItem::SectionHeader("Security"),
SettingsPageItem::SettingItem(SettingItem {
title: "Trust all worktrees by default",
description: r#"When opening a directory in Zed, whether to require confirmation to read and apply project settings"#,
field: Box::new(SettingField {
json_path: Some("session.trust_all_worktrees"),
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::SectionHeader("Workspace Restoration"),
SettingsPageItem::SettingItem(SettingItem { SettingsPageItem::SettingItem(SettingItem {
title: "Restore Unsaved Buffers", title: "Restore Unsaved Buffers",

View File

@@ -34,6 +34,7 @@ channel.workspace = true
chrono.workspace = true chrono.workspace = true
client.workspace = true client.workspace = true
cloud_llm_client.workspace = true cloud_llm_client.workspace = true
collections.workspace = true
db.workspace = true db.workspace = true
gpui = { workspace = true, features = ["screen-capture"] } gpui = { workspace = true, features = ["screen-capture"] }
notifications.workspace = true notifications.workspace = true
@@ -42,6 +43,7 @@ remote.workspace = true
rpc.workspace = true rpc.workspace = true
schemars.workspace = true schemars.workspace = true
serde.workspace = true serde.workspace = true
session.workspace = true
settings.workspace = true settings.workspace = true
smallvec.workspace = true smallvec.workspace = true
story = { workspace = true, optional = true } story = { workspace = true, optional = true }

View File

@@ -24,6 +24,7 @@ use auto_update::AutoUpdateStatus;
use call::ActiveCall; use call::ActiveCall;
use client::{Client, UserStore, zed_urls}; use client::{Client, UserStore, zed_urls};
use cloud_llm_client::{Plan, PlanV1, PlanV2}; use cloud_llm_client::{Plan, PlanV1, PlanV2};
use collections::HashSet;
use gpui::{ use gpui::{
Action, AnyElement, App, Context, Corner, Element, Entity, Focusable, InteractiveElement, Action, AnyElement, App, Context, Corner, Element, Entity, Focusable, InteractiveElement,
IntoElement, MouseButton, ParentElement, Render, StatefulInteractiveElement, Styled, IntoElement, MouseButton, ParentElement, Render, StatefulInteractiveElement, Styled,
@@ -32,8 +33,9 @@ use gpui::{
use onboarding_banner::OnboardingBanner; use onboarding_banner::OnboardingBanner;
use project::{Project, WorktreeSettings, git_store::GitStoreEvent}; use project::{Project, WorktreeSettings, git_store::GitStoreEvent};
use remote::RemoteConnectionOptions; use remote::RemoteConnectionOptions;
use session::TrustedWorktreesStorage;
use settings::{Settings, SettingsLocation}; use settings::{Settings, SettingsLocation};
use std::sync::Arc; use std::{path::PathBuf, sync::Arc};
use theme::ActiveTheme; use theme::ActiveTheme;
use title_bar_settings::TitleBarSettings; use title_bar_settings::TitleBarSettings;
use ui::{ use ui::{
@@ -134,6 +136,7 @@ pub struct TitleBar {
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
banner: Entity<OnboardingBanner>, banner: Entity<OnboardingBanner>,
screen_share_popover_handle: PopoverMenuHandle<ContextMenu>, screen_share_popover_handle: PopoverMenuHandle<ContextMenu>,
untrusted_worktrees: HashSet<PathBuf>,
} }
impl Render for TitleBar { impl Render for TitleBar {
@@ -163,6 +166,8 @@ impl Render for TitleBar {
title_bar title_bar
.when(title_bar_settings.show_project_items, |title_bar| { .when(title_bar_settings.show_project_items, |title_bar| {
title_bar title_bar
.pl_2()
.children(self.render_restricted_mode(cx))
.children(self.render_project_host(cx)) .children(self.render_project_host(cx))
.child(self.render_project_name(cx)) .child(self.render_project_name(cx))
}) })
@@ -290,6 +295,43 @@ impl TitleBar {
}), }),
); );
subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify())); subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
let mut untrusted_worktrees = if cx.has_global::<TrustedWorktreesStorage>() {
cx.update_global::<TrustedWorktreesStorage, _>(|trusted_worktrees_storage, cx| {
subscriptions.push(trusted_worktrees_storage.subscribe(
cx,
move |title_bar, e, cx| match e {
session::Event::TrustedWorktree(abs_path) => {
title_bar.untrusted_worktrees.remove(abs_path);
}
session::Event::UntrustedWorktree(abs_path) => {
title_bar
.workspace
.update(cx, |workspace, cx| {
if workspace
.project()
.read(cx)
.find_worktree(abs_path, cx)
.is_some()
{
title_bar.untrusted_worktrees.insert(abs_path.clone());
};
})
.ok();
}
},
));
trusted_worktrees_storage.untrusted_worktrees().clone()
})
} else {
HashSet::default()
};
untrusted_worktrees.retain(|untrusted_path| {
workspace
.project()
.read(cx)
.find_worktree(untrusted_path, cx)
.is_some()
});
let banner = cx.new(|cx| { let banner = cx.new(|cx| {
OnboardingBanner::new( OnboardingBanner::new(
@@ -315,7 +357,8 @@ impl TitleBar {
client, client,
_subscriptions: subscriptions, _subscriptions: subscriptions,
banner, banner,
screen_share_popover_handle: Default::default(), untrusted_worktrees,
screen_share_popover_handle: PopoverMenuHandle::default(),
} }
} }
@@ -398,6 +441,31 @@ impl TitleBar {
) )
} }
pub fn render_restricted_mode(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
if self.untrusted_worktrees.is_empty()
|| !cx.has_global::<session::TrustedWorktreesStorage>()
{
return None;
}
Some(
IconButton::new("restricted_mode_trigger", IconName::Warning)
.icon_color(Color::Warning)
.tooltip(Tooltip::text("Restricted Mode".to_string()))
.on_click({
cx.listener(move |title_bar, _, window, cx| {
title_bar
.workspace
.update(cx, |workspace, cx| {
workspace.show_worktree_security_modal(window, cx)
})
.log_err();
})
})
.into_any_element(),
)
}
pub fn render_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> { pub fn render_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
if self.project.read(cx).is_via_remote_server() { if self.project.read(cx).is_via_remote_server() {
return self.render_remote_project_connection(cx); return self.render_remote_project_connection(cx);

View File

@@ -1,73 +1,121 @@
use crate::component_prelude::*; use crate::component_prelude::*;
use crate::prelude::*; use crate::prelude::*;
use crate::{Checkbox, ListBulletItem, ToggleState};
use gpui::IntoElement; use gpui::IntoElement;
use smallvec::{SmallVec, smallvec}; use smallvec::{SmallVec, smallvec};
use theme::ActiveTheme;
#[derive(IntoElement, RegisterComponent)] #[derive(IntoElement, RegisterComponent)]
pub struct AlertModal { pub struct AlertModal {
id: ElementId, id: ElementId,
header: Option<AnyElement>,
children: SmallVec<[AnyElement; 2]>, children: SmallVec<[AnyElement; 2]>,
title: SharedString, footer: Option<AnyElement>,
primary_action: SharedString, title: Option<SharedString>,
dismiss_label: SharedString, primary_action: Option<SharedString>,
dismiss_label: Option<SharedString>,
width: Option<DefiniteLength>,
} }
impl AlertModal { impl AlertModal {
pub fn new(id: impl Into<ElementId>, title: impl Into<SharedString>) -> Self { pub fn new(id: impl Into<ElementId>) -> Self {
Self { Self {
id: id.into(), id: id.into(),
header: None,
children: smallvec![], children: smallvec![],
title: title.into(), footer: None,
primary_action: "Ok".into(), title: None,
dismiss_label: "Cancel".into(), primary_action: None,
dismiss_label: None,
width: None,
} }
} }
pub fn title(mut self, title: impl Into<SharedString>) -> 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<SharedString>) -> Self { pub fn primary_action(mut self, primary_action: impl Into<SharedString>) -> Self {
self.primary_action = primary_action.into(); self.primary_action = Some(primary_action.into());
self self
} }
pub fn dismiss_label(mut self, dismiss_label: impl Into<SharedString>) -> Self { pub fn dismiss_label(mut self, dismiss_label: impl Into<SharedString>) -> Self {
self.dismiss_label = dismiss_label.into(); self.dismiss_label = Some(dismiss_label.into());
self
}
pub fn width(mut self, width: impl Into<DefiniteLength>) -> Self {
self.width = Some(width.into());
self self
} }
} }
impl RenderOnce for AlertModal { impl RenderOnce for AlertModal {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { 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()
.id(self.id) .id(self.id)
.elevation_3(cx) .elevation_3(cx)
.w(px(440.)) .bg(cx.theme().colors().elevated_surface_background)
.p_5() .w(width)
.child( .overflow_hidden();
if let Some(header) = self.header {
modal = modal.child(header);
} else if let Some(title) = self.title {
modal = modal.child(
v_flex() 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_ui(cx)
.text_color(Color::Muted.color(cx)) .text_color(Color::Muted.color(cx))
.gap_1() .gap_1()
.child(Headline::new(self.title).size(HeadlineSize::Small))
.children(self.children), .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_flex()
.h(rems(1.75)) .p_3()
.items_center() .items_center()
.child(div().flex_1()) .justify_end()
.child( .gap_1()
h_flex() .child(Button::new(dismiss_label.clone(), dismiss_label).color(Color::Muted))
.items_center() .child(Button::new(primary_action.clone(), primary_action)),
.gap_1() );
.child( }
Button::new(self.dismiss_label.clone(), self.dismiss_label.clone())
.color(Color::Muted), modal
)
.child(Button::new(
self.primary_action.clone(),
self.primary_action,
)),
),
)
} }
} }
@@ -90,24 +138,75 @@ impl Component for AlertModal {
Some("A modal dialog that presents an alert message with primary and dismiss actions.") Some("A modal dialog that presents an alert message with primary and dismiss actions.")
} }
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> { fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
Some( Some(
v_flex() v_flex()
.gap_6() .gap_6()
.p_4() .p_4()
.children(vec![example_group( .children(vec![
vec![ example_group(vec![single_example(
single_example( "Basic Alert",
"Basic Alert", AlertModal::new("simple-modal")
AlertModal::new("simple-modal", "Do you want to leave the current call?") .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." .child(
) "The current window will be closed, and connections to any shared projects will be terminated."
.primary_action("Leave Call") )
.into_any_element(), .primary_action("Leave Call")
) .dismiss_label("Cancel")
], .into_any_element(),
)]) )]),
.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", "Open 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(),
) )
} }
} }

View File

@@ -0,0 +1,186 @@
use std::{
borrow::Cow,
path::{Path, PathBuf},
};
use collections::HashSet;
use gpui::BorrowAppContext;
use gpui::{DismissEvent, EventEmitter, Focusable};
use session::TrustedWorktreesStorage;
use theme::ActiveTheme;
use ui::{
AlertModal, Button, ButtonCommon as _, ButtonStyle, Checkbox, Clickable as _, Color, Context,
Headline, HeadlineSize, Icon, IconName, IconSize, IntoElement, Label, LabelCommon as _,
ListBulletItem, ParentElement as _, Render, Styled, ToggleState, Window, h_flex, rems, v_flex,
};
use crate::{DismissDecision, ModalView};
pub struct SecurityModal {
pub paths: HashSet<PathBuf>,
home_dir: Option<PathBuf>,
dismissed: bool,
trust_parents: bool,
}
impl Focusable for SecurityModal {
fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
cx.focus_handle()
}
}
impl EventEmitter<DismissEvent> for SecurityModal {}
impl ModalView for SecurityModal {
fn on_before_dismiss(
&mut self,
_window: &mut Window,
_: &mut Context<Self>,
) -> DismissDecision {
DismissDecision::Dismiss(self.dismissed)
}
fn fade_out_background(&self) -> bool {
true
}
}
impl Render for SecurityModal {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.paths.is_empty() {
self.dismiss(cx);
return v_flex().into_any_element();
}
let header_label = if self.paths.len() == 1 {
"Unrecognized Workspace"
} else {
"Unrecognized Workspaces"
};
let trust_label = self.build_trust_label();
AlertModal::new("security-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(header_label).size(HeadlineSize::Small)),
)
.children(self.paths.iter().map(|path| {
h_flex()
.pl(IconSize::default().rems() + rems(0.5))
.child(Label::new(path.display().to_string()).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_2()
.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-parents", ToggleState::from(self.trust_parents))
.label(trust_label)
.on_click(cx.listener(|security_modal, state: &ToggleState, _, cx| {
security_modal.trust_parents = state.selected();
cx.notify();
})),
)
.child(
h_flex()
.gap_1()
.child(
Button::new("open-in-restricted-mode", "Restricted Mode")
.color(Color::Muted)
.on_click(cx.listener(move |security_modal, _, _, cx| {
security_modal.dismiss(cx);
cx.stop_propagation();
})),
)
.child(
Button::new("trust-and-continue", "Trust and Continue")
.style(ButtonStyle::Filled)
.on_click(cx.listener(move |security_modal, _, _, cx| {
security_modal.trust_and_dismiss(cx);
})),
),
),
)
.width(rems(40.))
.into_any_element()
}
}
impl SecurityModal {
pub fn new(paths: HashSet<PathBuf>) -> Self {
Self {
paths,
dismissed: false,
trust_parents: false,
home_dir: std::env::home_dir(),
}
}
fn build_trust_label(&self) -> Cow<'static, str> {
if self.paths.len() == 1 {
let Some(single_path) = self.paths.iter().next() else {
return Cow::Borrowed("Trust all projects in the parent folders");
};
match single_path.parent().map(|path| match &self.home_dir {
Some(home_dir) => path
.strip_prefix(home_dir)
.map(|stripped| Path::new("~").join(stripped))
.map(Cow::Owned)
.unwrap_or(Cow::Borrowed(path)),
None => Cow::Borrowed(path),
}) {
Some(parent) => Cow::Owned(format!("Trust all projects in the {parent:?} folder")),
None => Cow::Borrowed("Trust all projects in the parent folders"),
}
} else {
Cow::Borrowed("Trust all projects in the parent folders")
}
}
fn trust_and_dismiss(&mut self, cx: &mut Context<Self>) {
if cx.has_global::<TrustedWorktreesStorage>() {
cx.update_global::<TrustedWorktreesStorage, _>(|trusted_wortrees_storage, cx| {
let mut paths_to_trust = self.paths.clone();
if self.trust_parents {
paths_to_trust.extend(
self.paths
.iter()
.filter_map(|path| Some(path.parent()?.to_owned())),
);
}
for path_to_trust in paths_to_trust {
trusted_wortrees_storage.trust_path(path_to_trust, cx);
}
});
}
self.dismiss(cx);
}
fn dismiss(&mut self, cx: &mut Context<Self>) {
self.dismissed = true;
cx.emit(DismissEvent);
}
}

View File

@@ -9,6 +9,7 @@ pub mod pane_group;
mod path_list; mod path_list;
mod persistence; mod persistence;
pub mod searchable; pub mod searchable;
mod security_modal;
pub mod shared_screen; pub mod shared_screen;
mod status_bar; mod status_bar;
pub mod tasks; pub mod tasks;
@@ -74,6 +75,7 @@ use project::{
DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId, DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
WorktreeSettings, WorktreeSettings,
debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus}, debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
project_settings::ProjectSettings,
toolchain_store::ToolchainStoreEvent, toolchain_store::ToolchainStoreEvent,
}; };
use remote::{ use remote::{
@@ -82,8 +84,10 @@ use remote::{
}; };
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use session::AppSession; use session::{AppSession, TrustedWorktreesStorage};
use settings::{CenteredPaddingSettings, Settings, SettingsLocation, update_settings_file}; use settings::{
CenteredPaddingSettings, Settings, SettingsLocation, SettingsStore, update_settings_file,
};
use shared_screen::SharedScreen; use shared_screen::SharedScreen;
use sqlez::{ use sqlez::{
bindable::{Bind, Column, StaticColumnCount}, bindable::{Bind, Column, StaticColumnCount},
@@ -126,11 +130,14 @@ pub use workspace_settings::{
}; };
use zed_actions::{Spawn, feedback::FileBugReport}; use zed_actions::{Spawn, feedback::FileBugReport};
use crate::persistence::{
SerializedAxis,
model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
};
use crate::{item::ItemBufferKind, notifications::NotificationId}; use crate::{item::ItemBufferKind, notifications::NotificationId};
use crate::{
persistence::{
SerializedAxis,
model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
},
security_modal::SecurityModal,
};
pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200); pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200);
@@ -265,6 +272,13 @@ actions!(
ToggleRightDock, ToggleRightDock,
/// Toggles zoom on the active pane. /// Toggles zoom on the active pane.
ToggleZoom, ToggleZoom,
/// If any worktrees are in restricted mode, shows a modal with possible actions.
/// TODO kb docs
ShowWorktreeSecurity,
/// Clears all trusted worktrees, placing them in restricted mode on next open.
/// Requires restart to take effect on already opened projects.
/// TODO kb docs
ClearTrustedWorktrees,
/// Stops following a collaborator. /// Stops following a collaborator.
Unfollow, Unfollow,
/// Restores the banner. /// Restores the banner.
@@ -1204,6 +1218,46 @@ impl Workspace {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
if cx.has_global::<TrustedWorktreesStorage>() {
cx.update_global::<TrustedWorktreesStorage, _>(|trusted_worktrees_storage, cx| {
// TODO kb Is it ok that remote projects' worktrees are identified by abs path only?
// Need to join with remote_hosts DB table data
trusted_worktrees_storage
.subscribe_in(window, cx, move |workspace, e, window, cx| match e {
session::Event::TrustedWorktree(trusted_path) => {
if let Some(security_modal) =
workspace.active_modal::<SecurityModal>(cx)
{
let remove = security_modal.update(cx, |security_modal, _| {
security_modal.paths.remove(trusted_path);
security_modal.paths.is_empty()
});
if remove {
workspace.hide_modal(window, cx);
}
}
}
session::Event::UntrustedWorktree(_) => {
workspace.show_worktree_security_modal(window, cx)
}
})
.detach();
})
};
cx.observe_global::<SettingsStore>(|_, cx| {
if ProjectSettings::get_global(cx).session.trust_all_worktrees {
if cx.has_global::<TrustedWorktreesStorage>() {
cx.update_global::<TrustedWorktreesStorage, _>(
|trusted_worktrees_storage, cx| {
trusted_worktrees_storage.trust_all(cx);
},
)
}
}
})
.detach();
cx.subscribe_in(&project, window, move |this, _, event, window, cx| { cx.subscribe_in(&project, window, move |this, _, event, window, cx| {
match event { match event {
project::Event::RemoteIdChanged(_) => { project::Event::RemoteIdChanged(_) => {
@@ -1461,9 +1515,10 @@ impl Workspace {
}), }),
]; ];
cx.defer_in(window, |this, window, cx| { cx.defer_in(window, move |workspace, window, cx| {
this.update_window_title(window, cx); workspace.update_window_title(window, cx);
this.show_initial_notifications(cx); workspace.show_initial_notifications(cx);
workspace.show_worktree_security_modal(window, cx);
}); });
Workspace { Workspace {
weak_self: weak_handle.clone(), weak_self: weak_handle.clone(),
@@ -1517,6 +1572,7 @@ impl Workspace {
scheduled_tasks: Vec::new(), scheduled_tasks: Vec::new(),
last_open_dock_positions: Vec::new(), last_open_dock_positions: Vec::new(),
removing: false, removing: false,
} }
} }
@@ -5912,6 +5968,22 @@ impl Workspace {
} }
}, },
)) ))
.on_action(cx.listener(
|workspace: &mut Workspace, _: &ShowWorktreeSecurity, window, cx| {
workspace.show_worktree_security_modal(window, cx);
},
))
.on_action(
cx.listener(|_: &mut Workspace, _: &ClearTrustedWorktrees, _, cx| {
if cx.has_global::<TrustedWorktreesStorage>() {
cx.update_global::<TrustedWorktreesStorage, _>(
|trusted_worktrees_storage, cx| {
trusted_worktrees_storage.clear_trusted_paths(cx);
},
);
}
}),
)
.on_action(cx.listener( .on_action(cx.listener(
|workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| { |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
workspace.reopen_closed_item(window, cx).detach(); workspace.reopen_closed_item(window, cx).detach();
@@ -6349,6 +6421,40 @@ impl Workspace {
file.project.all_languages.defaults.show_edit_predictions = Some(!show_edit_predictions) file.project.all_languages.defaults.show_edit_predictions = Some(!show_edit_predictions)
}); });
} }
pub fn show_worktree_security_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let untrusted_worktrees = if cx.has_global::<TrustedWorktreesStorage>() {
cx.update_global::<TrustedWorktreesStorage, _>(|trusted_worktrees_storage, cx| {
trusted_worktrees_storage
.untrusted_worktrees()
.iter()
.filter(|untrusted_path| {
self.project()
.read(cx)
.find_worktree(untrusted_path, cx)
.is_some()
})
.cloned()
.collect()
})
} else {
HashSet::default()
};
if let Some(security_modal) = self.active_modal::<SecurityModal>(cx) {
let remove = security_modal.update(cx, |security_modal, cx| {
security_modal.paths.extend(untrusted_worktrees);
let remove = security_modal.paths.is_empty();
cx.notify();
remove
});
if remove {
self.hide_modal(window, cx);
}
} else if !untrusted_worktrees.is_empty() {
self.toggle_modal(window, cx, |_, _| SecurityModal::new(untrusted_worktrees));
}
}
} }
fn leader_border_for_pane( fn leader_border_for_pane(

View File

@@ -105,6 +105,7 @@ impl Settings for WorkspaceSettings {
.collect(), .collect(),
close_on_file_delete: workspace.close_on_file_delete.unwrap(), close_on_file_delete: workspace.close_on_file_delete.unwrap(),
use_system_window_tabs: workspace.use_system_window_tabs.unwrap(), use_system_window_tabs: workspace.use_system_window_tabs.unwrap(),
zoomed_padding: workspace.zoomed_padding.unwrap(), zoomed_padding: workspace.zoomed_padding.unwrap(),
window_decorations: workspace.window_decorations.unwrap(), window_decorations: workspace.window_decorations.unwrap(),
} }

View File

@@ -404,6 +404,7 @@ pub fn main() {
}); });
app.run(move |cx| { app.run(move |cx| {
session::init(cx);
menu::init(); menu::init();
zed_actions::init(); zed_actions::init();

View File

@@ -1451,6 +1451,45 @@ or
`boolean` values `boolean` values
### Session
- Description: Controls Zed lifecycle-related behavior.
- Setting: `session`
- Default:
```json
{
"session": {
"restore_unsaved_buffers": true,
"trust_all_worktrees": false
}
}
```
**Options**
1. Whether or not to restore unsaved buffers on restart:
```json [settings]
{
"session": {
"restore_unsaved_buffers": true
}
}
```
If this is true, user won't be prompted whether to save/discard dirty files when closing the application.
2. Whether or not to skip project trust checks and synchronize project settings from any worktree automatically:
```json [settings]
{
"session": {
"trust_all_worktrees": false
}
}
```
### Drag And Drop Selection ### Drag And Drop Selection
- Description: Whether to allow drag and drop text selection in buffer. `delay` is the milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created. - Description: Whether to allow drag and drop text selection in buffer. `delay` is the milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created.