Compare commits
12 Commits
git-integr
...
security-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f34d12dd27 | ||
|
|
57b880526a | ||
|
|
039fe76a84 | ||
|
|
be7bfa1803 | ||
|
|
792641796a | ||
|
|
b14b869aa6 | ||
|
|
9473e69fff | ||
|
|
26945eea1c | ||
|
|
7cf39ed7e5 | ||
|
|
2ddd3a033f | ||
|
|
ddb7eb1747 | ||
|
|
db75a2c62a |
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -13076,6 +13076,7 @@ dependencies = [
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"session",
|
||||
"settings",
|
||||
"sha2",
|
||||
"shellexpand 2.1.2",
|
||||
@@ -15383,6 +15384,7 @@ dependencies = [
|
||||
name = "session"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"collections",
|
||||
"db",
|
||||
"gpui",
|
||||
"serde_json",
|
||||
@@ -17509,6 +17511,7 @@ dependencies = [
|
||||
"rpc",
|
||||
"schemars",
|
||||
"serde",
|
||||
"session",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"story",
|
||||
|
||||
@@ -2040,7 +2040,11 @@
|
||||
// dirty files when closing the application.
|
||||
//
|
||||
// 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.
|
||||
// Allows to enable/disable formatting with Prettier
|
||||
|
||||
@@ -70,6 +70,7 @@ schemars.workspace = true
|
||||
semver.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
session.workspace = true
|
||||
settings.workspace = true
|
||||
sha2.workspace = true
|
||||
shellexpand.workspace = true
|
||||
|
||||
@@ -17,13 +17,14 @@ use rpc::{
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use session::TrustedWorktreesStorage;
|
||||
pub use settings::DirenvSettings;
|
||||
pub use settings::LspSettings;
|
||||
use settings::{
|
||||
DapSettingsContent, InvalidSettingsError, LocalSettingsKind, RegisterSetting, Settings,
|
||||
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 util::{ResultExt, rel_path::RelPath, serde::default_true};
|
||||
use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
|
||||
@@ -83,6 +84,10 @@ pub struct SessionSettings {
|
||||
///
|
||||
/// Default: true
|
||||
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)]
|
||||
@@ -570,6 +575,7 @@ impl Settings for ProjectSettings {
|
||||
load_direnv: project.load_direnv.clone().unwrap(),
|
||||
session: SessionSettings {
|
||||
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>,
|
||||
project_id: u64,
|
||||
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>,
|
||||
_global_task_config_watcher: Task<()>,
|
||||
_global_debug_config_watcher: Task<()>,
|
||||
@@ -620,11 +628,65 @@ impl SettingsObserver {
|
||||
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
||||
.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 {
|
||||
worktree_store,
|
||||
task_store,
|
||||
mode: SettingsObserverMode::Local(fs.clone()),
|
||||
downstream_client: None,
|
||||
_trusted_worktrees_watcher,
|
||||
pending_local_settings: HashMap::default(),
|
||||
_user_settings_watcher: None,
|
||||
project_id: REMOTE_SERVER_PROJECT_ID,
|
||||
_global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
|
||||
@@ -677,6 +739,8 @@ impl SettingsObserver {
|
||||
mode: SettingsObserverMode::Remote,
|
||||
downstream_client: None,
|
||||
project_id: REMOTE_SERVER_PROJECT_ID,
|
||||
_trusted_worktrees_watcher: None,
|
||||
pending_local_settings: HashMap::default(),
|
||||
_user_settings_watcher: user_settings_watcher,
|
||||
_global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
|
||||
fs.clone(),
|
||||
@@ -968,36 +1032,36 @@ impl SettingsObserver {
|
||||
let worktree_id = worktree.read(cx).id();
|
||||
let remote_worktree_id = worktree.read(cx).id();
|
||||
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 {
|
||||
let mut applied = true;
|
||||
match kind {
|
||||
LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => 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()))));
|
||||
}
|
||||
LocalSettingsKind::Settings => {
|
||||
if *can_trust_worktree.get_or_init(|| {
|
||||
if cx.has_global::<TrustedWorktreesStorage>() {
|
||||
cx.update_global::<TrustedWorktreesStorage, _>(
|
||||
|trusted_worktrees_storage, cx| {
|
||||
trusted_worktrees_storage
|
||||
.can_trust_path(worktree_abs_path.as_ref(), cx)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}),
|
||||
}) {
|
||||
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 => {
|
||||
let result = task_store.update(cx, |task_store, cx| {
|
||||
task_store.update_user_tasks(
|
||||
@@ -1060,16 +1124,18 @@ impl SettingsObserver {
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(downstream_client) = &self.downstream_client {
|
||||
downstream_client
|
||||
.send(proto::UpdateWorktreeSettings {
|
||||
project_id: self.project_id,
|
||||
worktree_id: remote_worktree_id.to_proto(),
|
||||
path: directory.to_proto(),
|
||||
content: file_content.clone(),
|
||||
kind: Some(local_settings_kind_to_proto(kind).into()),
|
||||
})
|
||||
.log_err();
|
||||
if applied {
|
||||
if let Some(downstream_client) = &self.downstream_client {
|
||||
downstream_client
|
||||
.send(proto::UpdateWorktreeSettings {
|
||||
project_id: self.project_id,
|
||||
worktree_id: remote_worktree_id.to_proto(),
|
||||
path: directory.to_proto(),
|
||||
content: file_content.clone(),
|
||||
kind: Some(local_settings_kind_to_proto(kind).into()),
|
||||
})
|
||||
.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 {
|
||||
match kind {
|
||||
proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
|
||||
|
||||
@@ -18,6 +18,7 @@ test-support = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
gpui.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
@@ -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 gpui::{App, AppContext as _, Context, Subscription, Task, WindowId};
|
||||
use gpui::{
|
||||
App, AppContext as _, Context, Entity, EventEmitter, Global, Subscription, Task, Window,
|
||||
WindowId,
|
||||
};
|
||||
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 {
|
||||
session_id: String,
|
||||
old_session_id: Option<String>,
|
||||
@@ -12,6 +34,8 @@ pub struct Session {
|
||||
|
||||
const SESSION_ID_KEY: &str = "session_id";
|
||||
const SESSION_WINDOW_STACK_KEY: &str = "session_window_stack";
|
||||
const TRUSTED_WORKSPACES_KEY: &str = "trusted_workspaces";
|
||||
const TRUSTED_WORKSPACES_SEPARATOR: &str = "<|>";
|
||||
|
||||
impl Session {
|
||||
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>> {
|
||||
Some(
|
||||
cx.window_stack()?
|
||||
|
||||
@@ -187,6 +187,10 @@ pub struct SessionSettingsContent {
|
||||
///
|
||||
/// Default: true
|
||||
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)]
|
||||
|
||||
@@ -138,6 +138,28 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
|
||||
metadata: None,
|
||||
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::SettingItem(SettingItem {
|
||||
title: "Restore Unsaved Buffers",
|
||||
|
||||
@@ -34,6 +34,7 @@ channel.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
gpui = { workspace = true, features = ["screen-capture"] }
|
||||
notifications.workspace = true
|
||||
@@ -42,6 +43,7 @@ remote.workspace = true
|
||||
rpc.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
session.workspace = true
|
||||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
story = { workspace = true, optional = true }
|
||||
|
||||
@@ -24,6 +24,7 @@ use auto_update::AutoUpdateStatus;
|
||||
use call::ActiveCall;
|
||||
use client::{Client, UserStore, zed_urls};
|
||||
use cloud_llm_client::{Plan, PlanV1, PlanV2};
|
||||
use collections::HashSet;
|
||||
use gpui::{
|
||||
Action, AnyElement, App, Context, Corner, Element, Entity, Focusable, InteractiveElement,
|
||||
IntoElement, MouseButton, ParentElement, Render, StatefulInteractiveElement, Styled,
|
||||
@@ -32,8 +33,9 @@ use gpui::{
|
||||
use onboarding_banner::OnboardingBanner;
|
||||
use project::{Project, WorktreeSettings, git_store::GitStoreEvent};
|
||||
use remote::RemoteConnectionOptions;
|
||||
use session::TrustedWorktreesStorage;
|
||||
use settings::{Settings, SettingsLocation};
|
||||
use std::sync::Arc;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use theme::ActiveTheme;
|
||||
use title_bar_settings::TitleBarSettings;
|
||||
use ui::{
|
||||
@@ -134,6 +136,7 @@ pub struct TitleBar {
|
||||
_subscriptions: Vec<Subscription>,
|
||||
banner: Entity<OnboardingBanner>,
|
||||
screen_share_popover_handle: PopoverMenuHandle<ContextMenu>,
|
||||
untrusted_worktrees: HashSet<PathBuf>,
|
||||
}
|
||||
|
||||
impl Render for TitleBar {
|
||||
@@ -163,6 +166,8 @@ impl Render for TitleBar {
|
||||
title_bar
|
||||
.when(title_bar_settings.show_project_items, |title_bar| {
|
||||
title_bar
|
||||
.pl_2()
|
||||
.children(self.render_restricted_mode(cx))
|
||||
.children(self.render_project_host(cx))
|
||||
.child(self.render_project_name(cx))
|
||||
})
|
||||
@@ -290,6 +295,43 @@ impl TitleBar {
|
||||
}),
|
||||
);
|
||||
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| {
|
||||
OnboardingBanner::new(
|
||||
@@ -315,7 +357,8 @@ impl TitleBar {
|
||||
client,
|
||||
_subscriptions: subscriptions,
|
||||
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> {
|
||||
if self.project.read(cx).is_via_remote_server() {
|
||||
return self.render_remote_project_connection(cx);
|
||||
|
||||
@@ -1,73 +1,121 @@
|
||||
use crate::component_prelude::*;
|
||||
use crate::prelude::*;
|
||||
use crate::{Checkbox, ListBulletItem, ToggleState};
|
||||
use gpui::IntoElement;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct AlertModal {
|
||||
id: ElementId,
|
||||
header: Option<AnyElement>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
title: SharedString,
|
||||
primary_action: SharedString,
|
||||
dismiss_label: SharedString,
|
||||
footer: Option<AnyElement>,
|
||||
title: Option<SharedString>,
|
||||
primary_action: Option<SharedString>,
|
||||
dismiss_label: Option<SharedString>,
|
||||
width: Option<DefiniteLength>,
|
||||
}
|
||||
|
||||
impl AlertModal {
|
||||
pub fn new(id: impl Into<ElementId>, title: impl Into<SharedString>) -> Self {
|
||||
pub fn new(id: impl Into<ElementId>) -> 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,
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
self.primary_action = primary_action.into();
|
||||
self.primary_action = Some(primary_action.into());
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
.id(self.id)
|
||||
.elevation_3(cx)
|
||||
.w(px(440.))
|
||||
.p_5()
|
||||
.child(
|
||||
.bg(cx.theme().colors().elevated_surface_background)
|
||||
.w(width)
|
||||
.overflow_hidden();
|
||||
|
||||
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 +138,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<AnyElement> {
|
||||
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||
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", "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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
186
crates/workspace/src/security_modal.rs
Normal file
186
crates/workspace/src/security_modal.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ pub mod pane_group;
|
||||
mod path_list;
|
||||
mod persistence;
|
||||
pub mod searchable;
|
||||
mod security_modal;
|
||||
pub mod shared_screen;
|
||||
mod status_bar;
|
||||
pub mod tasks;
|
||||
@@ -74,6 +75,7 @@ use project::{
|
||||
DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
|
||||
WorktreeSettings,
|
||||
debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
|
||||
project_settings::ProjectSettings,
|
||||
toolchain_store::ToolchainStoreEvent,
|
||||
};
|
||||
use remote::{
|
||||
@@ -82,8 +84,10 @@ use remote::{
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use session::AppSession;
|
||||
use settings::{CenteredPaddingSettings, Settings, SettingsLocation, update_settings_file};
|
||||
use session::{AppSession, TrustedWorktreesStorage};
|
||||
use settings::{
|
||||
CenteredPaddingSettings, Settings, SettingsLocation, SettingsStore, update_settings_file,
|
||||
};
|
||||
use shared_screen::SharedScreen;
|
||||
use sqlez::{
|
||||
bindable::{Bind, Column, StaticColumnCount},
|
||||
@@ -126,11 +130,14 @@ pub use workspace_settings::{
|
||||
};
|
||||
use zed_actions::{Spawn, feedback::FileBugReport};
|
||||
|
||||
use crate::persistence::{
|
||||
SerializedAxis,
|
||||
model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
|
||||
};
|
||||
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);
|
||||
|
||||
@@ -265,6 +272,13 @@ actions!(
|
||||
ToggleRightDock,
|
||||
/// Toggles zoom on the active pane.
|
||||
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.
|
||||
Unfollow,
|
||||
/// Restores the banner.
|
||||
@@ -1204,6 +1218,46 @@ impl Workspace {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<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| {
|
||||
match event {
|
||||
project::Event::RemoteIdChanged(_) => {
|
||||
@@ -1461,9 +1515,10 @@ impl Workspace {
|
||||
}),
|
||||
];
|
||||
|
||||
cx.defer_in(window, |this, window, cx| {
|
||||
this.update_window_title(window, cx);
|
||||
this.show_initial_notifications(cx);
|
||||
cx.defer_in(window, move |workspace, window, cx| {
|
||||
workspace.update_window_title(window, cx);
|
||||
workspace.show_initial_notifications(cx);
|
||||
workspace.show_worktree_security_modal(window, cx);
|
||||
});
|
||||
Workspace {
|
||||
weak_self: weak_handle.clone(),
|
||||
@@ -1517,6 +1572,7 @@ impl Workspace {
|
||||
|
||||
scheduled_tasks: Vec::new(),
|
||||
last_open_dock_positions: Vec::new(),
|
||||
|
||||
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(
|
||||
|workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
|
||||
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)
|
||||
});
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@@ -105,6 +105,7 @@ impl Settings for WorkspaceSettings {
|
||||
.collect(),
|
||||
close_on_file_delete: workspace.close_on_file_delete.unwrap(),
|
||||
use_system_window_tabs: workspace.use_system_window_tabs.unwrap(),
|
||||
|
||||
zoomed_padding: workspace.zoomed_padding.unwrap(),
|
||||
window_decorations: workspace.window_decorations.unwrap(),
|
||||
}
|
||||
|
||||
@@ -404,6 +404,7 @@ pub fn main() {
|
||||
});
|
||||
|
||||
app.run(move |cx| {
|
||||
session::init(cx);
|
||||
menu::init();
|
||||
zed_actions::init();
|
||||
|
||||
|
||||
@@ -1451,6 +1451,45 @@ or
|
||||
|
||||
`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
|
||||
|
||||
- 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.
|
||||
|
||||
Reference in New Issue
Block a user