mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Introduce worktree trust mechanism (#44887)
Closes https://github.com/zed-industries/zed/issues/12589 Forces Zed to require user permissions before running any basic potentially dangerous actions: parsing and synchronizing `.zed/settings.json`, downloading and spawning any language and MCP servers (includes `prettier` and `copilot` instances) and all `NodeRuntime` interactions. There are more we can add later, among the ideas: DAP downloads on debugger start, Python virtual environment, etc. By default, Zed starts in restricted mode and shows a `! Restricted Mode` in the title bar, no aforementioned actions are executed. Clicking it or calling `workspace::ToggleWorktreeSecurity` command will bring a modal to trust worktrees or dismiss the modal: <img width="1341" height="475" alt="1" src="https://github.com/user-attachments/assets/4fabe63a-6494-42c7-b0ea-606abb1c0c20" /> Agent Panel shows a message too: <img width="644" height="106" alt="2" src="https://github.com/user-attachments/assets/0a4554bc-1f1e-455b-b97d-244d7d6a3259" /> This works on local, SSH and WSL remote projects, trusted worktrees are persisted between Zed restarts. There's a way to clear all persisted trust with `workspace::ClearTrustedWorktrees`, this will restart Zed. This mechanism can be turned off with settings: ```jsonc "session": { "trust_all_worktrees": true } ``` in this mode, all worktrees will be trusted by default, allowing all actions, but no auto trust will be persisted: hence, when the setting is changed back, auto trusted worktrees will require another trust confirmation. This settings switch was added to the onboarding view also. Release Notes: - Introduced worktree trust mechanism, can be turned off with `"session": { "trust_all_worktrees": true }` --------- Co-authored-by: Matt Miller <mattrx@gmail.com> Co-authored-by: Danilo Leal <daniloleal09@gmail.com> Co-authored-by: John D. Swanson <swanson.john.d@gmail.com>
This commit is contained in:
parent
93d79f3862
commit
f21cec7cb1
47 changed files with 4415 additions and 178 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -12421,6 +12421,7 @@ dependencies = [
|
|||
"context_server",
|
||||
"dap",
|
||||
"dap_adapters",
|
||||
"db",
|
||||
"extension",
|
||||
"fancy-regex",
|
||||
"fs",
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@
|
|||
"ctrl-alt-z": "edit_prediction::RatePredictions",
|
||||
"ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
|
||||
"ctrl-alt-l": "lsp_tool::ToggleMenu",
|
||||
"ctrl-alt-shift-s": "workspace::ToggleWorktreeSecurity",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@
|
|||
"ctrl-cmd-i": "edit_prediction::ToggleMenu",
|
||||
"ctrl-cmd-l": "lsp_tool::ToggleMenu",
|
||||
"ctrl-cmd-c": "editor::DisplayCursorNames",
|
||||
"ctrl-cmd-s": "workspace::ToggleWorktreeSecurity",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
"ctrl-shift-i": "edit_prediction::ToggleMenu",
|
||||
"shift-alt-l": "lsp_tool::ToggleMenu",
|
||||
"ctrl-shift-alt-c": "editor::DisplayCursorNames",
|
||||
"ctrl-shift-alt-s": "workspace::ToggleWorktreeSecurity",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2062,6 +2062,12 @@
|
|||
//
|
||||
// Default: true
|
||||
"restore_unsaved_buffers": true,
|
||||
// Whether or not to skip worktree trust checks.
|
||||
// When trusted, project settings are synchronized automatically,
|
||||
// language and MCP servers are downloaded and started automatically.
|
||||
//
|
||||
// Default: false
|
||||
"trust_all_worktrees": false,
|
||||
},
|
||||
// Zed's Prettier integration settings.
|
||||
// Allows to enable/disable formatting with Prettier
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@ use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration};
|
|||
|
||||
use acp_thread::AcpThread;
|
||||
use agent::{ContextServerRegistry, DbThreadMetadata, HistoryEntry, HistoryStore};
|
||||
use agent_servers::AgentServer;
|
||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||
use project::{
|
||||
ExternalAgentServerName,
|
||||
agent_server_store::{CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
|
||||
trusted_worktrees::{RemoteHostLocation, TrustedWorktrees, wait_for_workspace_trust},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{
|
||||
|
|
@ -262,6 +264,17 @@ impl AgentType {
|
|||
Self::Custom { .. } => Some(IconName::Sparkle),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_mcp(&self) -> bool {
|
||||
match self {
|
||||
Self::NativeAgent => false,
|
||||
Self::TextThread => false,
|
||||
Self::Custom { .. } => false,
|
||||
Self::Gemini => true,
|
||||
Self::ClaudeCode => true,
|
||||
Self::Codex => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ExternalAgent> for AgentType {
|
||||
|
|
@ -287,7 +300,7 @@ impl ActiveView {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn native_agent(
|
||||
fn native_agent(
|
||||
fs: Arc<dyn Fs>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
history_store: Entity<agent::HistoryStore>,
|
||||
|
|
@ -442,6 +455,9 @@ pub struct AgentPanel {
|
|||
pending_serialization: Option<Task<Result<()>>>,
|
||||
onboarding: Entity<AgentPanelOnboarding>,
|
||||
selected_agent: AgentType,
|
||||
new_agent_thread_task: Task<()>,
|
||||
show_trust_workspace_message: bool,
|
||||
_worktree_trust_subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl AgentPanel {
|
||||
|
|
@ -665,6 +681,48 @@ impl AgentPanel {
|
|||
None
|
||||
};
|
||||
|
||||
let mut show_trust_workspace_message = false;
|
||||
let worktree_trust_subscription =
|
||||
TrustedWorktrees::try_get_global(cx).and_then(|trusted_worktrees| {
|
||||
let has_global_trust = trusted_worktrees.update(cx, |trusted_worktrees, cx| {
|
||||
trusted_worktrees.can_trust_workspace(
|
||||
project
|
||||
.read(cx)
|
||||
.remote_connection_options(cx)
|
||||
.map(RemoteHostLocation::from),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
if has_global_trust {
|
||||
None
|
||||
} else {
|
||||
show_trust_workspace_message = true;
|
||||
let project = project.clone();
|
||||
Some(cx.subscribe(
|
||||
&trusted_worktrees,
|
||||
move |agent_panel, trusted_worktrees, _, cx| {
|
||||
let new_show_trust_workspace_message =
|
||||
!trusted_worktrees.update(cx, |trusted_worktrees, cx| {
|
||||
trusted_worktrees.can_trust_workspace(
|
||||
project
|
||||
.read(cx)
|
||||
.remote_connection_options(cx)
|
||||
.map(RemoteHostLocation::from),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
if new_show_trust_workspace_message
|
||||
!= agent_panel.show_trust_workspace_message
|
||||
{
|
||||
agent_panel.show_trust_workspace_message =
|
||||
new_show_trust_workspace_message;
|
||||
cx.notify();
|
||||
};
|
||||
},
|
||||
))
|
||||
}
|
||||
});
|
||||
|
||||
let mut panel = Self {
|
||||
active_view,
|
||||
workspace,
|
||||
|
|
@ -687,11 +745,14 @@ impl AgentPanel {
|
|||
height: None,
|
||||
zoomed: false,
|
||||
pending_serialization: None,
|
||||
new_agent_thread_task: Task::ready(()),
|
||||
onboarding,
|
||||
acp_history,
|
||||
history_store,
|
||||
selected_agent: AgentType::default(),
|
||||
loading: false,
|
||||
show_trust_workspace_message,
|
||||
_worktree_trust_subscription: worktree_trust_subscription,
|
||||
};
|
||||
|
||||
// Initial sync of agent servers from extensions
|
||||
|
|
@ -884,37 +945,63 @@ impl AgentPanel {
|
|||
}
|
||||
};
|
||||
|
||||
let server = ext_agent.server(fs, history);
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
let selected_agent = ext_agent.into();
|
||||
if this.selected_agent != selected_agent {
|
||||
this.selected_agent = selected_agent;
|
||||
this.serialize(cx);
|
||||
if ext_agent.is_mcp() {
|
||||
let wait_task = this.update(cx, |agent_panel, cx| {
|
||||
agent_panel.project.update(cx, |project, cx| {
|
||||
wait_for_workspace_trust(
|
||||
project.remote_connection_options(cx),
|
||||
"context servers",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?;
|
||||
if let Some(wait_task) = wait_task {
|
||||
this.update_in(cx, |agent_panel, window, cx| {
|
||||
agent_panel.show_trust_workspace_message = true;
|
||||
cx.notify();
|
||||
agent_panel.new_agent_thread_task =
|
||||
cx.spawn_in(window, async move |agent_panel, cx| {
|
||||
wait_task.await;
|
||||
let server = ext_agent.server(fs, history);
|
||||
agent_panel
|
||||
.update_in(cx, |agent_panel, window, cx| {
|
||||
agent_panel.show_trust_workspace_message = false;
|
||||
cx.notify();
|
||||
agent_panel._external_thread(
|
||||
server,
|
||||
resume_thread,
|
||||
summarize_thread,
|
||||
workspace,
|
||||
project,
|
||||
loading,
|
||||
ext_agent,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
})?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let thread_view = cx.new(|cx| {
|
||||
crate::acp::AcpThreadView::new(
|
||||
server,
|
||||
resume_thread,
|
||||
summarize_thread,
|
||||
workspace.clone(),
|
||||
project,
|
||||
this.history_store.clone(),
|
||||
this.prompt_store.clone(),
|
||||
!loading,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
this.set_active_view(
|
||||
ActiveView::ExternalAgentThread { thread_view },
|
||||
!loading,
|
||||
let server = ext_agent.server(fs, history);
|
||||
this.update_in(cx, |agent_panel, window, cx| {
|
||||
agent_panel._external_thread(
|
||||
server,
|
||||
resume_thread,
|
||||
summarize_thread,
|
||||
workspace,
|
||||
project,
|
||||
loading,
|
||||
ext_agent,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
|
@ -1423,6 +1510,36 @@ impl AgentPanel {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let wait_task = if agent.is_mcp() {
|
||||
self.project.update(cx, |project, cx| {
|
||||
wait_for_workspace_trust(
|
||||
project.remote_connection_options(cx),
|
||||
"context servers",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(wait_task) = wait_task {
|
||||
self.show_trust_workspace_message = true;
|
||||
cx.notify();
|
||||
self.new_agent_thread_task = cx.spawn_in(window, async move |agent_panel, cx| {
|
||||
wait_task.await;
|
||||
agent_panel
|
||||
.update_in(cx, |agent_panel, window, cx| {
|
||||
agent_panel.show_trust_workspace_message = false;
|
||||
cx.notify();
|
||||
agent_panel._new_agent_thread(agent, window, cx);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
} else {
|
||||
self._new_agent_thread(agent, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn _new_agent_thread(&mut self, agent: AgentType, window: &mut Window, cx: &mut Context<Self>) {
|
||||
match agent {
|
||||
AgentType::TextThread => {
|
||||
window.dispatch_action(NewTextThread.boxed_clone(), cx);
|
||||
|
|
@ -1477,6 +1594,47 @@ impl AgentPanel {
|
|||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn _external_thread(
|
||||
&mut self,
|
||||
server: Rc<dyn AgentServer>,
|
||||
resume_thread: Option<DbThreadMetadata>,
|
||||
summarize_thread: Option<DbThreadMetadata>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
loading: bool,
|
||||
ext_agent: ExternalAgent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let selected_agent = AgentType::from(ext_agent);
|
||||
if self.selected_agent != selected_agent {
|
||||
self.selected_agent = selected_agent;
|
||||
self.serialize(cx);
|
||||
}
|
||||
|
||||
let thread_view = cx.new(|cx| {
|
||||
crate::acp::AcpThreadView::new(
|
||||
server,
|
||||
resume_thread,
|
||||
summarize_thread,
|
||||
workspace.clone(),
|
||||
project,
|
||||
self.history_store.clone(),
|
||||
self.prompt_store.clone(),
|
||||
!loading,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
self.set_active_view(
|
||||
ActiveView::ExternalAgentThread { thread_view },
|
||||
!loading,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for AgentPanel {
|
||||
|
|
@ -2557,6 +2715,38 @@ impl AgentPanel {
|
|||
}
|
||||
}
|
||||
|
||||
fn render_workspace_trust_message(&self, cx: &Context<Self>) -> Option<impl IntoElement> {
|
||||
if !self.show_trust_workspace_message {
|
||||
return None;
|
||||
}
|
||||
|
||||
let description = "To protect your system, third-party code—like MCP servers—won't run until you mark this workspace as safe.";
|
||||
|
||||
Some(
|
||||
Callout::new()
|
||||
.icon(IconName::Warning)
|
||||
.severity(Severity::Warning)
|
||||
.border_position(ui::BorderPosition::Bottom)
|
||||
.title("You're in Restricted Mode")
|
||||
.description(description)
|
||||
.actions_slot(
|
||||
Button::new("open-trust-modal", "Configure Project Trust")
|
||||
.label_size(LabelSize::Small)
|
||||
.style(ButtonStyle::Outlined)
|
||||
.on_click({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.show_worktree_trust_security_modal(true, window, cx)
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn key_context(&self) -> KeyContext {
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
key_context.add("AgentPanel");
|
||||
|
|
@ -2609,6 +2799,7 @@ impl Render for AgentPanel {
|
|||
}
|
||||
}))
|
||||
.child(self.render_toolbar(window, cx))
|
||||
.children(self.render_workspace_trust_message(cx))
|
||||
.children(self.render_onboarding(window, cx))
|
||||
.map(|parent| match &self.active_view {
|
||||
ActiveView::ExternalAgentThread { thread_view, .. } => parent
|
||||
|
|
|
|||
|
|
@ -171,6 +171,16 @@ impl ExternalAgent {
|
|||
Self::Custom { name } => Rc::new(agent_servers::CustomAgentServer::new(name.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_mcp(&self) -> bool {
|
||||
match self {
|
||||
Self::Gemini => true,
|
||||
Self::ClaudeCode => true,
|
||||
Self::Codex => true,
|
||||
Self::NativeAgent => false,
|
||||
Self::Custom { .. } => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the profile management interface for configuring agent tools and settings.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use collections::{HashMap, HashSet};
|
|||
|
||||
use dap::{Capabilities, adapters::DebugTaskDefinition, transport::RequestHandling};
|
||||
use debugger_ui::debugger_panel::DebugPanel;
|
||||
use editor::{Editor, EditorMode, MultiBuffer};
|
||||
use extension::ExtensionHostProxy;
|
||||
use fs::{FakeFs, Fs as _, RemoveOptions};
|
||||
use futures::StreamExt as _;
|
||||
|
|
@ -12,22 +13,30 @@ use http_client::BlockedHttpClient;
|
|||
use language::{
|
||||
FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
|
||||
language_settings::{Formatter, FormatterList, language_settings},
|
||||
tree_sitter_typescript,
|
||||
rust_lang, tree_sitter_typescript,
|
||||
};
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::{
|
||||
ProjectPath,
|
||||
debugger::session::ThreadId,
|
||||
lsp_store::{FormatTrigger, LspFormatTarget},
|
||||
trusted_worktrees::{PathTrust, TrustedWorktrees},
|
||||
};
|
||||
use remote::RemoteClient;
|
||||
use remote_server::{HeadlessAppState, HeadlessProject};
|
||||
use rpc::proto;
|
||||
use serde_json::json;
|
||||
use settings::{LanguageServerFormatterSpecifier, PrettierSettingsContent, SettingsStore};
|
||||
use settings::{
|
||||
InlayHintSettingsContent, LanguageServerFormatterSpecifier, PrettierSettingsContent,
|
||||
SettingsStore,
|
||||
};
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::{Arc, atomic::AtomicUsize},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use task::TcpArgumentsTemplate;
|
||||
use util::{path, rel_path::rel_path};
|
||||
|
|
@ -90,13 +99,14 @@ async fn test_sharing_an_ssh_remote_project(
|
|||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let (project_a, worktree_id) = client_a
|
||||
.build_ssh_project(path!("/code/project1"), client_ssh, cx_a)
|
||||
.build_ssh_project(path!("/code/project1"), client_ssh, false, cx_a)
|
||||
.await;
|
||||
|
||||
// While the SSH worktree is being scanned, user A shares the remote project.
|
||||
|
|
@ -250,13 +260,14 @@ async fn test_ssh_collaboration_git_branches(
|
|||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let (project_a, _) = client_a
|
||||
.build_ssh_project("/project", client_ssh, cx_a)
|
||||
.build_ssh_project("/project", client_ssh, false, cx_a)
|
||||
.await;
|
||||
|
||||
// While the SSH worktree is being scanned, user A shares the remote project.
|
||||
|
|
@ -454,13 +465,14 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
|||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let (project_a, worktree_id) = client_a
|
||||
.build_ssh_project(path!("/project"), client_ssh, cx_a)
|
||||
.build_ssh_project(path!("/project"), client_ssh, false, cx_a)
|
||||
.await;
|
||||
|
||||
// While the SSH worktree is being scanned, user A shares the remote project.
|
||||
|
|
@ -615,6 +627,7 @@ async fn test_remote_server_debugger(
|
|||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
|
@ -627,7 +640,7 @@ async fn test_remote_server_debugger(
|
|||
command_palette_hooks::init(cx);
|
||||
});
|
||||
let (project_a, _) = client_a
|
||||
.build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
|
||||
.build_ssh_project(path!("/code"), client_ssh.clone(), false, cx_a)
|
||||
.await;
|
||||
|
||||
let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||
|
|
@ -723,6 +736,7 @@ async fn test_slow_adapter_startup_retries(
|
|||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
|
@ -735,7 +749,7 @@ async fn test_slow_adapter_startup_retries(
|
|||
command_palette_hooks::init(cx);
|
||||
});
|
||||
let (project_a, _) = client_a
|
||||
.build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
|
||||
.build_ssh_project(path!("/code"), client_ssh.clone(), false, cx_a)
|
||||
.await;
|
||||
|
||||
let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||
|
|
@ -838,3 +852,259 @@ async fn test_slow_adapter_startup_retries(
|
|||
|
||||
shutdown_session.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
||||
use project::trusted_worktrees::RemoteHostLocation;
|
||||
|
||||
cx_a.update(|cx| {
|
||||
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||
});
|
||||
server_cx.update(|cx| {
|
||||
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||
});
|
||||
|
||||
let mut server = TestServer::start(cx_a.executor().clone()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
|
||||
let server_name = "override-rust-analyzer";
|
||||
let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
|
||||
let remote_fs = FakeFs::new(server_cx.executor());
|
||||
remote_fs
|
||||
.insert_tree(
|
||||
path!("/projects"),
|
||||
json!({
|
||||
"project_a": {
|
||||
".zed": {
|
||||
"settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
|
||||
},
|
||||
"main.rs": "fn main() {}"
|
||||
},
|
||||
"project_b": { "lib.rs": "pub fn lib() {}" }
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
server_cx.update(HeadlessProject::init);
|
||||
let remote_http_client = Arc::new(BlockedHttpClient);
|
||||
let node = NodeRuntime::unavailable();
|
||||
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
|
||||
languages.add(rust_lang());
|
||||
|
||||
let capabilities = lsp::ServerCapabilities {
|
||||
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
|
||||
..lsp::ServerCapabilities::default()
|
||||
};
|
||||
let mut fake_language_servers = languages.register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
name: server_name,
|
||||
capabilities: capabilities.clone(),
|
||||
initializer: Some(Box::new({
|
||||
let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
|
||||
move |fake_server| {
|
||||
let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
|
||||
fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
|
||||
move |_params, _| {
|
||||
lsp_inlay_hint_request_count.fetch_add(1, Ordering::Release);
|
||||
async move {
|
||||
Ok(Some(vec![lsp::InlayHint {
|
||||
position: lsp::Position::new(0, 0),
|
||||
label: lsp::InlayHintLabel::String("hint".to_string()),
|
||||
kind: None,
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: None,
|
||||
padding_right: None,
|
||||
data: None,
|
||||
}]))
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
})),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
|
||||
let _headless_project = server_cx.new(|cx| {
|
||||
HeadlessProject::new(
|
||||
HeadlessAppState {
|
||||
session: server_ssh,
|
||||
fs: remote_fs.clone(),
|
||||
http_client: remote_http_client,
|
||||
node_runtime: node,
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
true,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let (project_a, worktree_id_a) = client_a
|
||||
.build_ssh_project(path!("/projects/project_a"), client_ssh.clone(), true, cx_a)
|
||||
.await;
|
||||
|
||||
cx_a.update(|cx| {
|
||||
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
let language_settings = &mut settings.project.all_languages.defaults;
|
||||
language_settings.inlay_hints = Some(InlayHintSettingsContent {
|
||||
enabled: Some(true),
|
||||
..InlayHintSettingsContent::default()
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
project_a
|
||||
.update(cx_a, |project, cx| {
|
||||
project.languages().add(rust_lang());
|
||||
project.languages().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
name: server_name,
|
||||
capabilities,
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
project.find_or_create_worktree(path!("/projects/project_b"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx_a.run_until_parked();
|
||||
|
||||
let worktree_ids = project_a.read_with(cx_a, |project, cx| {
|
||||
project
|
||||
.worktrees(cx)
|
||||
.map(|wt| wt.read(cx).id())
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
assert_eq!(worktree_ids.len(), 2);
|
||||
|
||||
let remote_host = project_a.read_with(cx_a, |project, cx| {
|
||||
project
|
||||
.remote_connection_options(cx)
|
||||
.map(RemoteHostLocation::from)
|
||||
});
|
||||
|
||||
let trusted_worktrees =
|
||||
cx_a.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
|
||||
|
||||
let can_trust_a =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[0], cx));
|
||||
let can_trust_b =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[1], cx));
|
||||
assert!(!can_trust_a, "project_a should be restricted initially");
|
||||
assert!(!can_trust_b, "project_b should be restricted initially");
|
||||
|
||||
let worktree_store = project_a.read_with(cx_a, |project, _| project.worktree_store());
|
||||
let has_restricted = trusted_worktrees.read_with(cx_a, |store, cx| {
|
||||
store.has_restricted_worktrees(&worktree_store, cx)
|
||||
});
|
||||
assert!(has_restricted, "should have restricted worktrees");
|
||||
|
||||
let buffer_before_approval = project_a
|
||||
.update(cx_a, |project, cx| {
|
||||
project.open_buffer((worktree_id_a, rel_path("main.rs")), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (editor, cx_a) = cx_a.add_window_view(|window, cx| {
|
||||
Editor::new(
|
||||
EditorMode::full(),
|
||||
cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
|
||||
Some(project_a.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx_a.run_until_parked();
|
||||
let fake_language_server = fake_language_servers.next();
|
||||
|
||||
cx_a.read(|cx| {
|
||||
let file = buffer_before_approval.read(cx).file();
|
||||
assert_eq!(
|
||||
language_settings(Some("Rust".into()), file, cx).language_servers,
|
||||
["...".to_string()],
|
||||
"remote .zed/settings.json must not sync before trust approval"
|
||||
)
|
||||
});
|
||||
|
||||
editor.update_in(cx_a, |editor, window, cx| {
|
||||
editor.handle_input("1", window, cx);
|
||||
});
|
||||
cx_a.run_until_parked();
|
||||
cx_a.executor().advance_clock(Duration::from_secs(1));
|
||||
assert_eq!(
|
||||
lsp_inlay_hint_request_count.load(Ordering::Acquire),
|
||||
0,
|
||||
"inlay hints must not be queried before trust approval"
|
||||
);
|
||||
|
||||
trusted_worktrees.update(cx_a, |store, cx| {
|
||||
store.trust(
|
||||
HashSet::from_iter([PathTrust::Worktree(worktree_ids[0])]),
|
||||
remote_host.clone(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx_a.run_until_parked();
|
||||
|
||||
cx_a.read(|cx| {
|
||||
let file = buffer_before_approval.read(cx).file();
|
||||
assert_eq!(
|
||||
language_settings(Some("Rust".into()), file, cx).language_servers,
|
||||
["override-rust-analyzer".to_string()],
|
||||
"remote .zed/settings.json should sync after trust approval"
|
||||
)
|
||||
});
|
||||
let _fake_language_server = fake_language_server.await.unwrap();
|
||||
editor.update_in(cx_a, |editor, window, cx| {
|
||||
editor.handle_input("1", window, cx);
|
||||
});
|
||||
cx_a.run_until_parked();
|
||||
cx_a.executor().advance_clock(Duration::from_secs(1));
|
||||
assert!(
|
||||
lsp_inlay_hint_request_count.load(Ordering::Acquire) > 0,
|
||||
"inlay hints should be queried after trust approval"
|
||||
);
|
||||
|
||||
let can_trust_a =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[0], cx));
|
||||
let can_trust_b =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[1], cx));
|
||||
assert!(can_trust_a, "project_a should be trusted after trust()");
|
||||
assert!(!can_trust_b, "project_b should still be restricted");
|
||||
|
||||
trusted_worktrees.update(cx_a, |store, cx| {
|
||||
store.trust(
|
||||
HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
|
||||
remote_host.clone(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let can_trust_a =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[0], cx));
|
||||
let can_trust_b =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[1], cx));
|
||||
assert!(can_trust_a, "project_a should remain trusted");
|
||||
assert!(can_trust_b, "project_b should now be trusted");
|
||||
|
||||
let has_restricted_after = trusted_worktrees.read_with(cx_a, |store, cx| {
|
||||
store.has_restricted_worktrees(&worktree_store, cx)
|
||||
});
|
||||
assert!(
|
||||
!has_restricted_after,
|
||||
"should have no restricted worktrees after trusting both"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -761,6 +761,7 @@ impl TestClient {
|
|||
&self,
|
||||
root_path: impl AsRef<Path>,
|
||||
ssh: Entity<RemoteClient>,
|
||||
init_worktree_trust: bool,
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Entity<Project>, WorktreeId) {
|
||||
let project = cx.update(|cx| {
|
||||
|
|
@ -771,6 +772,7 @@ impl TestClient {
|
|||
self.app_state.user_store.clone(),
|
||||
self.app_state.languages.clone(),
|
||||
self.app_state.fs.clone(),
|
||||
init_worktree_trust,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
|
@ -839,6 +841,7 @@ impl TestClient {
|
|||
self.app_state.languages.clone(),
|
||||
self.app_state.fs.clone(),
|
||||
None,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@ use gpui_tokio::Tokio;
|
|||
use language::LanguageRegistry;
|
||||
use language_extension::LspAccess;
|
||||
use node_runtime::{NodeBinaryOptions, NodeRuntime};
|
||||
use project::Project;
|
||||
use project::project_settings::ProjectSettings;
|
||||
use project::{Project, project_settings::ProjectSettings};
|
||||
use release_channel::{AppCommitSha, AppVersion};
|
||||
use reqwest_client::ReqwestClient;
|
||||
use settings::{Settings, SettingsStore};
|
||||
|
|
@ -115,7 +114,7 @@ pub fn init(cx: &mut App) -> EpAppState {
|
|||
tx.send(Some(options)).log_err();
|
||||
})
|
||||
.detach();
|
||||
let node_runtime = NodeRuntime::new(client.http_client(), None, rx);
|
||||
let node_runtime = NodeRuntime::new(client.http_client(), None, rx, None);
|
||||
|
||||
let extension_host_proxy = ExtensionHostProxy::global(cx);
|
||||
|
||||
|
|
|
|||
|
|
@ -179,6 +179,7 @@ async fn setup_project(
|
|||
app_state.languages.clone(),
|
||||
app_state.fs.clone(),
|
||||
None,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
|
|
|||
|
|
@ -41,14 +41,16 @@ use multi_buffer::{
|
|||
use parking_lot::Mutex;
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
use project::{
|
||||
FakeFs,
|
||||
FakeFs, Project,
|
||||
debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
|
||||
project_settings::LspSettings,
|
||||
trusted_worktrees::{PathTrust, TrustedWorktrees},
|
||||
};
|
||||
use serde_json::{self, json};
|
||||
use settings::{
|
||||
AllLanguageSettingsContent, EditorSettingsContent, IndentGuideBackgroundColoring,
|
||||
IndentGuideColoring, ProjectSettingsContent, SearchSettingsContent,
|
||||
IndentGuideColoring, InlayHintSettingsContent, ProjectSettingsContent, SearchSettingsContent,
|
||||
SettingsStore,
|
||||
};
|
||||
use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
|
||||
use std::{
|
||||
|
|
@ -29364,3 +29366,166 @@ async fn test_find_references_single_case(cx: &mut TestAppContext) {
|
|||
|
||||
cx.assert_editor_state(after);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_local_worktree_trust(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.all_languages.defaults.inlay_hints =
|
||||
Some(InlayHintSettingsContent {
|
||||
enabled: Some(true),
|
||||
..InlayHintSettingsContent::default()
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
".zed": {
|
||||
"settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
|
||||
},
|
||||
"main.rs": "fn main() {}"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
|
||||
let server_name = "override-rust-analyzer";
|
||||
let project = Project::test_with_worktree_trust(fs, [path!("/project").as_ref()], cx).await;
|
||||
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
language_registry.add(rust_lang());
|
||||
|
||||
let capabilities = lsp::ServerCapabilities {
|
||||
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
|
||||
..lsp::ServerCapabilities::default()
|
||||
};
|
||||
let mut fake_language_servers = language_registry.register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
name: server_name,
|
||||
capabilities,
|
||||
initializer: Some(Box::new({
|
||||
let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
|
||||
move |fake_server| {
|
||||
let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
|
||||
fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
|
||||
move |_params, _| {
|
||||
lsp_inlay_hint_request_count.fetch_add(1, atomic::Ordering::Release);
|
||||
async move {
|
||||
Ok(Some(vec![lsp::InlayHint {
|
||||
position: lsp::Position::new(0, 0),
|
||||
label: lsp::InlayHintLabel::String("hint".to_string()),
|
||||
kind: None,
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: None,
|
||||
padding_right: None,
|
||||
data: None,
|
||||
}]))
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
})),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let worktree_id = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.worktrees(cx)
|
||||
.next()
|
||||
.map(|wt| wt.read(cx).id())
|
||||
.expect("should have a worktree")
|
||||
});
|
||||
|
||||
let trusted_worktrees =
|
||||
cx.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
|
||||
|
||||
let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
|
||||
assert!(!can_trust, "worktree should be restricted initially");
|
||||
|
||||
let buffer_before_approval = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer((worktree_id, rel_path("main.rs")), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| {
|
||||
Editor::new(
|
||||
EditorMode::full(),
|
||||
cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
|
||||
Some(project.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let fake_language_server = fake_language_servers.next();
|
||||
|
||||
cx.read(|cx| {
|
||||
let file = buffer_before_approval.read(cx).file();
|
||||
assert_eq!(
|
||||
language::language_settings::language_settings(Some("Rust".into()), file, cx)
|
||||
.language_servers,
|
||||
["...".to_string()],
|
||||
"local .zed/settings.json must not apply before trust approval"
|
||||
)
|
||||
});
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.handle_input("1", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.executor()
|
||||
.advance_clock(std::time::Duration::from_secs(1));
|
||||
assert_eq!(
|
||||
lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire),
|
||||
0,
|
||||
"inlay hints must not be queried before trust approval"
|
||||
);
|
||||
|
||||
trusted_worktrees.update(cx, |store, cx| {
|
||||
store.trust(
|
||||
std::collections::HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.read(|cx| {
|
||||
let file = buffer_before_approval.read(cx).file();
|
||||
assert_eq!(
|
||||
language::language_settings::language_settings(Some("Rust".into()), file, cx)
|
||||
.language_servers,
|
||||
["override-rust-analyzer".to_string()],
|
||||
"local .zed/settings.json should apply after trust approval"
|
||||
)
|
||||
});
|
||||
let _fake_language_server = fake_language_server.await.unwrap();
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.handle_input("1", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.executor()
|
||||
.advance_clock(std::time::Duration::from_secs(1));
|
||||
assert!(
|
||||
lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire) > 0,
|
||||
"inlay hints should be queried after trust approval"
|
||||
);
|
||||
|
||||
let can_trust_after =
|
||||
trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
|
||||
assert!(can_trust_after, "worktree should be trusted after trust()");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -422,7 +422,7 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
|
|||
tx.send(Some(options)).log_err();
|
||||
})
|
||||
.detach();
|
||||
let node_runtime = NodeRuntime::new(client.http_client(), None, rx);
|
||||
let node_runtime = NodeRuntime::new(client.http_client(), None, rx, None);
|
||||
|
||||
let extension_host_proxy = ExtensionHostProxy::global(cx);
|
||||
debug_adapter_extension::init(extension_host_proxy.clone(), cx);
|
||||
|
|
|
|||
|
|
@ -202,6 +202,7 @@ impl ExampleInstance {
|
|||
app_state.languages.clone(),
|
||||
app_state.fs.clone(),
|
||||
None,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -421,6 +421,7 @@ async fn open_remote_worktree(
|
|||
app_state.user_store.clone(),
|
||||
app_state.languages.clone(),
|
||||
app_state.fs.clone(),
|
||||
true,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
|||
app_state.languages.clone(),
|
||||
app_state.fs.clone(),
|
||||
None,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ use serde::Deserialize;
|
|||
use smol::io::BufReader;
|
||||
use smol::{fs, lock::Mutex};
|
||||
use std::fmt::Display;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::{
|
||||
env::{self, consts},
|
||||
ffi::OsString,
|
||||
|
|
@ -46,6 +48,7 @@ struct NodeRuntimeState {
|
|||
last_options: Option<NodeBinaryOptions>,
|
||||
options: watch::Receiver<Option<NodeBinaryOptions>>,
|
||||
shell_env_loaded: Shared<oneshot::Receiver<()>>,
|
||||
trust_task: Option<Pin<Box<dyn Future<Output = ()> + Send>>>,
|
||||
}
|
||||
|
||||
impl NodeRuntime {
|
||||
|
|
@ -53,9 +56,11 @@ impl NodeRuntime {
|
|||
http: Arc<dyn HttpClient>,
|
||||
shell_env_loaded: Option<oneshot::Receiver<()>>,
|
||||
options: watch::Receiver<Option<NodeBinaryOptions>>,
|
||||
trust_task: Option<Pin<Box<dyn Future<Output = ()> + Send>>>,
|
||||
) -> Self {
|
||||
NodeRuntime(Arc::new(Mutex::new(NodeRuntimeState {
|
||||
http,
|
||||
trust_task,
|
||||
instance: None,
|
||||
last_options: None,
|
||||
options,
|
||||
|
|
@ -70,11 +75,15 @@ impl NodeRuntime {
|
|||
last_options: None,
|
||||
options: watch::channel(Some(NodeBinaryOptions::default())).1,
|
||||
shell_env_loaded: oneshot::channel().1.shared(),
|
||||
trust_task: None,
|
||||
})))
|
||||
}
|
||||
|
||||
async fn instance(&self) -> Box<dyn NodeRuntimeTrait> {
|
||||
let mut state = self.0.lock().await;
|
||||
if let Some(trust_task) = state.trust_task.take() {
|
||||
trust_task.await;
|
||||
}
|
||||
|
||||
let options = loop {
|
||||
if let Some(options) = state.options.borrow().as_ref() {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use std::sync::Arc;
|
|||
use client::TelemetrySettings;
|
||||
use fs::Fs;
|
||||
use gpui::{Action, App, IntoElement};
|
||||
use project::project_settings::ProjectSettings;
|
||||
use settings::{BaseKeymap, Settings, update_settings_file};
|
||||
use theme::{
|
||||
Appearance, SystemAppearance, ThemeAppearanceMode, ThemeName, ThemeRegistry, ThemeSelection,
|
||||
|
|
@ -10,8 +11,8 @@ use theme::{
|
|||
};
|
||||
use ui::{
|
||||
Divider, ParentElement as _, StatefulInteractiveElement, SwitchField, TintColor,
|
||||
ToggleButtonGroup, ToggleButtonGroupSize, ToggleButtonSimple, ToggleButtonWithIcon, prelude::*,
|
||||
rems_from_px,
|
||||
ToggleButtonGroup, ToggleButtonGroupSize, ToggleButtonSimple, ToggleButtonWithIcon, Tooltip,
|
||||
prelude::*, rems_from_px,
|
||||
};
|
||||
use vim_mode_setting::VimModeSetting;
|
||||
|
||||
|
|
@ -409,6 +410,48 @@ fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoEleme
|
|||
})
|
||||
}
|
||||
|
||||
fn render_worktree_auto_trust_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
|
||||
let toggle_state = if ProjectSettings::get_global(cx).session.trust_all_worktrees {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
};
|
||||
|
||||
let tooltip_description = "Zed can only allow services like language servers, project settings, and MCP servers to run after you mark a new project as trusted.";
|
||||
|
||||
SwitchField::new(
|
||||
"onboarding-auto-trust-worktrees",
|
||||
Some("Trust All Projects By Default"),
|
||||
Some("Automatically mark all new projects as trusted to unlock all Zed's features".into()),
|
||||
toggle_state,
|
||||
{
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
move |&selection, _, cx| {
|
||||
let trust = match selection {
|
||||
ToggleState::Selected => true,
|
||||
ToggleState::Unselected => false,
|
||||
ToggleState::Indeterminate => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
update_settings_file(fs.clone(), cx, move |setting, _| {
|
||||
setting.session.get_or_insert_default().trust_all_worktrees = Some(trust);
|
||||
});
|
||||
|
||||
telemetry::event!(
|
||||
"Welcome Page Worktree Auto Trust Toggled",
|
||||
options = if trust { "on" } else { "off" }
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
.tab_index({
|
||||
*tab_index += 1;
|
||||
*tab_index - 1
|
||||
})
|
||||
.tooltip(Tooltip::text(tooltip_description))
|
||||
}
|
||||
|
||||
fn render_setting_import_button(
|
||||
tab_index: isize,
|
||||
label: SharedString,
|
||||
|
|
@ -481,6 +524,7 @@ pub(crate) fn render_basics_page(cx: &mut App) -> impl IntoElement {
|
|||
.child(render_base_keymap_section(&mut tab_index, cx))
|
||||
.child(render_import_settings_section(&mut tab_index, cx))
|
||||
.child(render_vim_mode_switch(&mut tab_index, cx))
|
||||
.child(render_worktree_auto_trust_switch(&mut tab_index, cx))
|
||||
.child(Divider::horizontal().color(ui::DividerColor::BorderVariant))
|
||||
.child(render_telemetry_section(&mut tab_index, cx))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ clock.workspace = true
|
|||
collections.workspace = true
|
||||
context_server.workspace = true
|
||||
dap.workspace = true
|
||||
db.workspace = true
|
||||
extension.workspace = true
|
||||
fancy-regex.workspace = true
|
||||
fs.workspace = true
|
||||
|
|
@ -96,6 +97,7 @@ tracing.workspace = true
|
|||
|
||||
[dev-dependencies]
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
db = { workspace = true, features = ["test-support"] }
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
context_server = { workspace = true, features = ["test-support"] }
|
||||
buffer_diff = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ use util::{ResultExt as _, rel_path::RelPath};
|
|||
use crate::{
|
||||
Project,
|
||||
project_settings::{ContextServerSettings, ProjectSettings},
|
||||
trusted_worktrees::wait_for_workspace_trust,
|
||||
worktree_store::WorktreeStore,
|
||||
};
|
||||
|
||||
|
|
@ -332,6 +333,15 @@ impl ContextServerStore {
|
|||
|
||||
pub fn start_server(&mut self, server: Arc<ContextServer>, cx: &mut Context<Self>) {
|
||||
cx.spawn(async move |this, cx| {
|
||||
let wait_task = this.update(cx, |context_server_store, cx| {
|
||||
context_server_store.project.update(cx, |project, cx| {
|
||||
let remote_host = project.remote_connection_options(cx);
|
||||
wait_for_workspace_trust(remote_host, "context servers", cx)
|
||||
})
|
||||
})??;
|
||||
if let Some(wait_task) = wait_task {
|
||||
wait_task.await;
|
||||
}
|
||||
let this = this.upgrade().context("Context server store dropped")?;
|
||||
let settings = this
|
||||
.update(cx, |this, _| {
|
||||
|
|
@ -572,6 +582,15 @@ impl ContextServerStore {
|
|||
}
|
||||
|
||||
async fn maintain_servers(this: WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {
|
||||
let wait_task = this.update(cx, |context_server_store, cx| {
|
||||
context_server_store.project.update(cx, |project, cx| {
|
||||
let remote_host = project.remote_connection_options(cx);
|
||||
wait_for_workspace_trust(remote_host, "context servers", cx)
|
||||
})
|
||||
})??;
|
||||
if let Some(wait_task) = wait_task {
|
||||
wait_task.await;
|
||||
}
|
||||
let (mut configured_servers, registry, worktree_store) = this.update(cx, |this, _| {
|
||||
(
|
||||
this.context_server_settings.clone(),
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ use crate::{
|
|||
prettier_store::{self, PrettierStore, PrettierStoreEvent},
|
||||
project_settings::{LspSettings, ProjectSettings},
|
||||
toolchain_store::{LocalToolchainStore, ToolchainStoreEvent},
|
||||
trusted_worktrees::{PathTrust, TrustedWorktrees, TrustedWorktreesEvent},
|
||||
worktree_store::{WorktreeStore, WorktreeStoreEvent},
|
||||
yarn::YarnPathStore,
|
||||
};
|
||||
|
|
@ -54,8 +55,8 @@ use futures::{
|
|||
};
|
||||
use globset::{Glob, GlobBuilder, GlobMatcher, GlobSet, GlobSetBuilder};
|
||||
use gpui::{
|
||||
App, AppContext, AsyncApp, Context, Entity, EventEmitter, PromptLevel, SharedString, Task,
|
||||
WeakEntity,
|
||||
App, AppContext, AsyncApp, Context, Entity, EventEmitter, PromptLevel, SharedString,
|
||||
Subscription, Task, WeakEntity,
|
||||
};
|
||||
use http_client::HttpClient;
|
||||
use itertools::Itertools as _;
|
||||
|
|
@ -96,13 +97,14 @@ use serde::Serialize;
|
|||
use serde_json::Value;
|
||||
use settings::{Settings, SettingsLocation, SettingsStore};
|
||||
use sha2::{Digest, Sha256};
|
||||
use smol::channel::Sender;
|
||||
use smol::channel::{Receiver, Sender};
|
||||
use snippet::Snippet;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
borrow::Cow,
|
||||
cell::RefCell,
|
||||
cmp::{Ordering, Reverse},
|
||||
collections::hash_map,
|
||||
convert::TryInto,
|
||||
ffi::OsStr,
|
||||
future::ready,
|
||||
|
|
@ -296,6 +298,7 @@ pub struct LocalLspStore {
|
|||
LanguageServerId,
|
||||
HashMap<Option<SharedString>, HashMap<PathBuf, Option<SharedString>>>,
|
||||
>,
|
||||
restricted_worktrees_tasks: HashMap<WorktreeId, (Subscription, Receiver<()>)>,
|
||||
}
|
||||
|
||||
impl LocalLspStore {
|
||||
|
|
@ -367,7 +370,8 @@ impl LocalLspStore {
|
|||
) -> LanguageServerId {
|
||||
let worktree = worktree_handle.read(cx);
|
||||
|
||||
let root_path = worktree.abs_path();
|
||||
let worktree_id = worktree.id();
|
||||
let worktree_abs_path = worktree.abs_path();
|
||||
let toolchain = key.toolchain.clone();
|
||||
let override_options = settings.initialization_options.clone();
|
||||
|
||||
|
|
@ -375,19 +379,49 @@ impl LocalLspStore {
|
|||
|
||||
let server_id = self.languages.next_language_server_id();
|
||||
log::trace!(
|
||||
"attempting to start language server {:?}, path: {root_path:?}, id: {server_id}",
|
||||
"attempting to start language server {:?}, path: {worktree_abs_path:?}, id: {server_id}",
|
||||
adapter.name.0
|
||||
);
|
||||
|
||||
let untrusted_worktree_task =
|
||||
TrustedWorktrees::try_get_global(cx).and_then(|trusted_worktrees| {
|
||||
let can_trust = trusted_worktrees.update(cx, |trusted_worktrees, cx| {
|
||||
trusted_worktrees.can_trust(worktree_id, cx)
|
||||
});
|
||||
if can_trust {
|
||||
self.restricted_worktrees_tasks.remove(&worktree_id);
|
||||
None
|
||||
} else {
|
||||
match self.restricted_worktrees_tasks.entry(worktree_id) {
|
||||
hash_map::Entry::Occupied(o) => Some(o.get().1.clone()),
|
||||
hash_map::Entry::Vacant(v) => {
|
||||
let (tx, rx) = smol::channel::bounded::<()>(1);
|
||||
let subscription = cx.subscribe(&trusted_worktrees, move |_, e, _| {
|
||||
if let TrustedWorktreesEvent::Trusted(_, trusted_paths) = e {
|
||||
if trusted_paths.contains(&PathTrust::Worktree(worktree_id)) {
|
||||
tx.send_blocking(()).ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
v.insert((subscription, rx.clone()));
|
||||
Some(rx)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let update_binary_status = untrusted_worktree_task.is_none();
|
||||
|
||||
let binary = self.get_language_server_binary(
|
||||
worktree_abs_path.clone(),
|
||||
adapter.clone(),
|
||||
settings,
|
||||
toolchain.clone(),
|
||||
delegate.clone(),
|
||||
true,
|
||||
untrusted_worktree_task,
|
||||
cx,
|
||||
);
|
||||
let pending_workspace_folders: Arc<Mutex<BTreeSet<Uri>>> = Default::default();
|
||||
let pending_workspace_folders = Arc::<Mutex<BTreeSet<Uri>>>::default();
|
||||
|
||||
let pending_server = cx.spawn({
|
||||
let adapter = adapter.clone();
|
||||
|
|
@ -420,7 +454,7 @@ impl LocalLspStore {
|
|||
server_id,
|
||||
server_name,
|
||||
binary,
|
||||
&root_path,
|
||||
&worktree_abs_path,
|
||||
code_action_kinds,
|
||||
Some(pending_workspace_folders),
|
||||
cx,
|
||||
|
|
@ -556,8 +590,10 @@ impl LocalLspStore {
|
|||
pending_workspace_folders,
|
||||
};
|
||||
|
||||
self.languages
|
||||
.update_lsp_binary_status(adapter.name(), BinaryStatus::Starting);
|
||||
if update_binary_status {
|
||||
self.languages
|
||||
.update_lsp_binary_status(adapter.name(), BinaryStatus::Starting);
|
||||
}
|
||||
|
||||
self.language_servers.insert(server_id, state);
|
||||
self.language_server_ids
|
||||
|
|
@ -571,19 +607,34 @@ impl LocalLspStore {
|
|||
|
||||
fn get_language_server_binary(
|
||||
&self,
|
||||
worktree_abs_path: Arc<Path>,
|
||||
adapter: Arc<CachedLspAdapter>,
|
||||
settings: Arc<LspSettings>,
|
||||
toolchain: Option<Toolchain>,
|
||||
delegate: Arc<dyn LspAdapterDelegate>,
|
||||
allow_binary_download: bool,
|
||||
untrusted_worktree_task: Option<Receiver<()>>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<LanguageServerBinary>> {
|
||||
if let Some(settings) = &settings.binary
|
||||
&& let Some(path) = settings.path.as_ref().map(PathBuf::from)
|
||||
{
|
||||
let settings = settings.clone();
|
||||
|
||||
let languages = self.languages.clone();
|
||||
return cx.background_spawn(async move {
|
||||
if let Some(untrusted_worktree_task) = untrusted_worktree_task {
|
||||
log::info!(
|
||||
"Waiting for worktree {worktree_abs_path:?} to be trusted, before starting language server {}",
|
||||
adapter.name(),
|
||||
);
|
||||
untrusted_worktree_task.recv().await.ok();
|
||||
log::info!(
|
||||
"Worktree {worktree_abs_path:?} is trusted, starting language server {}",
|
||||
adapter.name(),
|
||||
);
|
||||
languages
|
||||
.update_lsp_binary_status(adapter.name(), BinaryStatus::Starting);
|
||||
}
|
||||
let mut env = delegate.shell_env().await;
|
||||
env.extend(settings.env.unwrap_or_default());
|
||||
|
||||
|
|
@ -614,6 +665,18 @@ impl LocalLspStore {
|
|||
};
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
if let Some(untrusted_worktree_task) = untrusted_worktree_task {
|
||||
log::info!(
|
||||
"Waiting for worktree {worktree_abs_path:?} to be trusted, before starting language server {}",
|
||||
adapter.name(),
|
||||
);
|
||||
untrusted_worktree_task.recv().await.ok();
|
||||
log::info!(
|
||||
"Worktree {worktree_abs_path:?} is trusted, starting language server {}",
|
||||
adapter.name(),
|
||||
);
|
||||
}
|
||||
|
||||
let (existing_binary, maybe_download_binary) = adapter
|
||||
.clone()
|
||||
.get_language_server_command(delegate.clone(), toolchain, lsp_binary_options, cx)
|
||||
|
|
@ -3258,6 +3321,7 @@ impl LocalLspStore {
|
|||
id_to_remove: WorktreeId,
|
||||
cx: &mut Context<LspStore>,
|
||||
) -> Vec<LanguageServerId> {
|
||||
self.restricted_worktrees_tasks.remove(&id_to_remove);
|
||||
self.diagnostics.remove(&id_to_remove);
|
||||
self.prettier_store.update(cx, |prettier_store, cx| {
|
||||
prettier_store.remove_worktree(id_to_remove, cx);
|
||||
|
|
@ -3974,6 +4038,7 @@ impl LspStore {
|
|||
buffers_opened_in_servers: HashMap::default(),
|
||||
buffer_pull_diagnostics_result_ids: HashMap::default(),
|
||||
workspace_pull_diagnostics_result_ids: HashMap::default(),
|
||||
restricted_worktrees_tasks: HashMap::default(),
|
||||
watched_manifest_filenames: ManifestProvidersStore::global(cx)
|
||||
.manifest_file_names(),
|
||||
}),
|
||||
|
|
|
|||
411
crates/project/src/persistence.rs
Normal file
411
crates/project/src/persistence.rs
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
use collections::{HashMap, HashSet};
|
||||
use gpui::{App, Entity, SharedString};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use db::{
|
||||
query,
|
||||
sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
trusted_worktrees::{PathTrust, RemoteHostLocation, find_worktree_in_store},
|
||||
worktree_store::WorktreeStore,
|
||||
};
|
||||
|
||||
// https://www.sqlite.org/limits.html
|
||||
// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
|
||||
// > which defaults to <..> 32766 for SQLite versions after 3.32.0.
|
||||
#[allow(unused)]
|
||||
const MAX_QUERY_PLACEHOLDERS: usize = 32000;
|
||||
|
||||
#[allow(unused)]
|
||||
pub struct ProjectDb(ThreadSafeConnection);
|
||||
|
||||
impl Domain for ProjectDb {
|
||||
const NAME: &str = stringify!(ProjectDb);
|
||||
|
||||
const MIGRATIONS: &[&str] = &[sql!(
|
||||
CREATE TABLE IF NOT EXISTS trusted_worktrees (
|
||||
trust_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
absolute_path TEXT,
|
||||
user_name TEXT,
|
||||
host_name TEXT
|
||||
) STRICT;
|
||||
)];
|
||||
}
|
||||
|
||||
db::static_connection!(PROJECT_DB, ProjectDb, []);
|
||||
|
||||
impl ProjectDb {
|
||||
pub(crate) async fn save_trusted_worktrees(
|
||||
&self,
|
||||
trusted_worktrees: HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>,
|
||||
trusted_workspaces: HashSet<Option<RemoteHostLocation>>,
|
||||
) -> anyhow::Result<()> {
|
||||
use anyhow::Context as _;
|
||||
use db::sqlez::statement::Statement;
|
||||
use itertools::Itertools as _;
|
||||
|
||||
PROJECT_DB
|
||||
.clear_trusted_worktrees()
|
||||
.await
|
||||
.context("clearing previous trust state")?;
|
||||
|
||||
let trusted_worktrees = trusted_worktrees
|
||||
.into_iter()
|
||||
.flat_map(|(host, abs_paths)| {
|
||||
abs_paths
|
||||
.into_iter()
|
||||
.map(move |abs_path| (Some(abs_path), host.clone()))
|
||||
})
|
||||
.chain(trusted_workspaces.into_iter().map(|host| (None, host)))
|
||||
.collect::<Vec<_>>();
|
||||
let mut first_worktree;
|
||||
let mut last_worktree = 0_usize;
|
||||
for (count, placeholders) in std::iter::once("(?, ?, ?)")
|
||||
.cycle()
|
||||
.take(trusted_worktrees.len())
|
||||
.chunks(MAX_QUERY_PLACEHOLDERS / 3)
|
||||
.into_iter()
|
||||
.map(|chunk| {
|
||||
let mut count = 0;
|
||||
let placeholders = chunk
|
||||
.inspect(|_| {
|
||||
count += 1;
|
||||
})
|
||||
.join(", ");
|
||||
(count, placeholders)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
first_worktree = last_worktree;
|
||||
last_worktree = last_worktree + count;
|
||||
let query = format!(
|
||||
r#"INSERT INTO trusted_worktrees(absolute_path, user_name, host_name)
|
||||
VALUES {placeholders};"#
|
||||
);
|
||||
|
||||
let trusted_worktrees = trusted_worktrees[first_worktree..last_worktree].to_vec();
|
||||
self.write(move |conn| {
|
||||
let mut statement = Statement::prepare(conn, query)?;
|
||||
let mut next_index = 1;
|
||||
for (abs_path, host) in trusted_worktrees {
|
||||
let abs_path = abs_path.as_ref().map(|abs_path| abs_path.to_string_lossy());
|
||||
next_index = statement.bind(
|
||||
&abs_path.as_ref().map(|abs_path| abs_path.as_ref()),
|
||||
next_index,
|
||||
)?;
|
||||
next_index = statement.bind(
|
||||
&host
|
||||
.as_ref()
|
||||
.and_then(|host| Some(host.user_name.as_ref()?.as_str())),
|
||||
next_index,
|
||||
)?;
|
||||
next_index = statement.bind(
|
||||
&host.as_ref().map(|host| host.host_identifier.as_str()),
|
||||
next_index,
|
||||
)?;
|
||||
}
|
||||
statement.exec()
|
||||
})
|
||||
.await
|
||||
.context("inserting new trusted state")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn fetch_trusted_worktrees(
|
||||
&self,
|
||||
worktree_store: Option<Entity<WorktreeStore>>,
|
||||
host: Option<RemoteHostLocation>,
|
||||
cx: &App,
|
||||
) -> anyhow::Result<HashMap<Option<RemoteHostLocation>, HashSet<PathTrust>>> {
|
||||
let trusted_worktrees = PROJECT_DB.trusted_worktrees()?;
|
||||
Ok(trusted_worktrees
|
||||
.into_iter()
|
||||
.map(|(abs_path, user_name, host_name)| {
|
||||
let db_host = match (user_name, host_name) {
|
||||
(_, None) => None,
|
||||
(None, Some(host_name)) => Some(RemoteHostLocation {
|
||||
user_name: None,
|
||||
host_identifier: SharedString::new(host_name),
|
||||
}),
|
||||
(Some(user_name), Some(host_name)) => Some(RemoteHostLocation {
|
||||
user_name: Some(SharedString::new(user_name)),
|
||||
host_identifier: SharedString::new(host_name),
|
||||
}),
|
||||
};
|
||||
|
||||
match abs_path {
|
||||
Some(abs_path) => {
|
||||
if db_host != host {
|
||||
(db_host, PathTrust::AbsPath(abs_path))
|
||||
} else if let Some(worktree_store) = &worktree_store {
|
||||
find_worktree_in_store(worktree_store.read(cx), &abs_path, cx)
|
||||
.map(PathTrust::Worktree)
|
||||
.map(|trusted_worktree| (host.clone(), trusted_worktree))
|
||||
.unwrap_or_else(|| (db_host.clone(), PathTrust::AbsPath(abs_path)))
|
||||
} else {
|
||||
(db_host, PathTrust::AbsPath(abs_path))
|
||||
}
|
||||
}
|
||||
None => (db_host, PathTrust::Workspace),
|
||||
}
|
||||
})
|
||||
.fold(HashMap::default(), |mut acc, (remote_host, path_trust)| {
|
||||
acc.entry(remote_host)
|
||||
.or_insert_with(HashSet::default)
|
||||
.insert(path_trust);
|
||||
acc
|
||||
}))
|
||||
}
|
||||
|
||||
query! {
|
||||
fn trusted_worktrees() -> Result<Vec<(Option<PathBuf>, Option<String>, Option<String>)>> {
|
||||
SELECT absolute_path, user_name, host_name
|
||||
FROM trusted_worktrees
|
||||
}
|
||||
}
|
||||
|
||||
query! {
|
||||
pub async fn clear_trusted_worktrees() -> Result<()> {
|
||||
DELETE FROM trusted_worktrees
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use collections::{HashMap, HashSet};
|
||||
use gpui::{SharedString, TestAppContext};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use smol::lock::Mutex;
|
||||
use util::path;
|
||||
|
||||
use crate::{
|
||||
FakeFs, Project,
|
||||
persistence::PROJECT_DB,
|
||||
trusted_worktrees::{PathTrust, RemoteHostLocation},
|
||||
};
|
||||
|
||||
static TEST_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_save_and_fetch_trusted_worktrees(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
let _guard = TEST_LOCK.lock().await;
|
||||
PROJECT_DB.clear_trusted_worktrees().await.unwrap();
|
||||
cx.update(|cx| {
|
||||
if cx.try_global::<SettingsStore>().is_none() {
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
}
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/"),
|
||||
json!({
|
||||
"project_a": { "main.rs": "" },
|
||||
"project_b": { "lib.rs": "" }
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(
|
||||
fs,
|
||||
[path!("/project_a").as_ref(), path!("/project_b").as_ref()],
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let worktree_store = project.read_with(cx, |p, _| p.worktree_store());
|
||||
|
||||
let mut trusted_paths: HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>> =
|
||||
HashMap::default();
|
||||
trusted_paths.insert(
|
||||
None,
|
||||
HashSet::from_iter([
|
||||
PathBuf::from(path!("/project_a")),
|
||||
PathBuf::from(path!("/project_b")),
|
||||
]),
|
||||
);
|
||||
|
||||
PROJECT_DB
|
||||
.save_trusted_worktrees(trusted_paths, HashSet::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fetched = cx.update(|cx| {
|
||||
PROJECT_DB.fetch_trusted_worktrees(Some(worktree_store.clone()), None, cx)
|
||||
});
|
||||
let fetched = fetched.unwrap();
|
||||
|
||||
let local_trust = fetched.get(&None).expect("should have local host entry");
|
||||
assert_eq!(local_trust.len(), 2);
|
||||
assert!(
|
||||
local_trust
|
||||
.iter()
|
||||
.all(|p| matches!(p, PathTrust::Worktree(_)))
|
||||
);
|
||||
|
||||
let fetched_no_store = cx
|
||||
.update(|cx| PROJECT_DB.fetch_trusted_worktrees(None, None, cx))
|
||||
.unwrap();
|
||||
let local_trust_no_store = fetched_no_store
|
||||
.get(&None)
|
||||
.expect("should have local host entry");
|
||||
assert_eq!(local_trust_no_store.len(), 2);
|
||||
assert!(
|
||||
local_trust_no_store
|
||||
.iter()
|
||||
.all(|p| matches!(p, PathTrust::AbsPath(_)))
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_save_and_fetch_workspace_trust(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
let _guard = TEST_LOCK.lock().await;
|
||||
PROJECT_DB.clear_trusted_worktrees().await.unwrap();
|
||||
cx.update(|cx| {
|
||||
if cx.try_global::<SettingsStore>().is_none() {
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
}
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/root"), json!({ "main.rs": "" }))
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
|
||||
let worktree_store = project.read_with(cx, |p, _| p.worktree_store());
|
||||
|
||||
let trusted_workspaces = HashSet::from_iter([None]);
|
||||
PROJECT_DB
|
||||
.save_trusted_worktrees(HashMap::default(), trusted_workspaces)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fetched = cx.update(|cx| {
|
||||
PROJECT_DB.fetch_trusted_worktrees(Some(worktree_store.clone()), None, cx)
|
||||
});
|
||||
let fetched = fetched.unwrap();
|
||||
|
||||
let local_trust = fetched.get(&None).expect("should have local host entry");
|
||||
assert!(local_trust.contains(&PathTrust::Workspace));
|
||||
|
||||
let fetched_no_store = cx
|
||||
.update(|cx| PROJECT_DB.fetch_trusted_worktrees(None, None, cx))
|
||||
.unwrap();
|
||||
let local_trust_no_store = fetched_no_store
|
||||
.get(&None)
|
||||
.expect("should have local host entry");
|
||||
assert!(local_trust_no_store.contains(&PathTrust::Workspace));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_save_and_fetch_remote_host_trust(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
let _guard = TEST_LOCK.lock().await;
|
||||
PROJECT_DB.clear_trusted_worktrees().await.unwrap();
|
||||
cx.update(|cx| {
|
||||
if cx.try_global::<SettingsStore>().is_none() {
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
}
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/root"), json!({ "main.rs": "" }))
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
|
||||
let worktree_store = project.read_with(cx, |p, _| p.worktree_store());
|
||||
|
||||
let remote_host = Some(RemoteHostLocation {
|
||||
user_name: Some(SharedString::from("testuser")),
|
||||
host_identifier: SharedString::from("remote.example.com"),
|
||||
});
|
||||
|
||||
let mut trusted_paths: HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>> =
|
||||
HashMap::default();
|
||||
trusted_paths.insert(
|
||||
remote_host.clone(),
|
||||
HashSet::from_iter([PathBuf::from("/home/testuser/project")]),
|
||||
);
|
||||
|
||||
PROJECT_DB
|
||||
.save_trusted_worktrees(trusted_paths, HashSet::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fetched = cx.update(|cx| {
|
||||
PROJECT_DB.fetch_trusted_worktrees(Some(worktree_store.clone()), None, cx)
|
||||
});
|
||||
let fetched = fetched.unwrap();
|
||||
|
||||
let remote_trust = fetched
|
||||
.get(&remote_host)
|
||||
.expect("should have remote host entry");
|
||||
assert_eq!(remote_trust.len(), 1);
|
||||
assert!(remote_trust
|
||||
.iter()
|
||||
.any(|p| matches!(p, PathTrust::AbsPath(path) if path == &PathBuf::from("/home/testuser/project"))));
|
||||
|
||||
let fetched_no_store = cx
|
||||
.update(|cx| PROJECT_DB.fetch_trusted_worktrees(None, None, cx))
|
||||
.unwrap();
|
||||
let remote_trust_no_store = fetched_no_store
|
||||
.get(&remote_host)
|
||||
.expect("should have remote host entry");
|
||||
assert_eq!(remote_trust_no_store.len(), 1);
|
||||
assert!(remote_trust_no_store
|
||||
.iter()
|
||||
.any(|p| matches!(p, PathTrust::AbsPath(path) if path == &PathBuf::from("/home/testuser/project"))));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_clear_trusted_worktrees(cx: &mut TestAppContext) {
|
||||
cx.executor().allow_parking();
|
||||
let _guard = TEST_LOCK.lock().await;
|
||||
PROJECT_DB.clear_trusted_worktrees().await.unwrap();
|
||||
cx.update(|cx| {
|
||||
if cx.try_global::<SettingsStore>().is_none() {
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
}
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/root"), json!({ "main.rs": "" }))
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
|
||||
let worktree_store = project.read_with(cx, |p, _| p.worktree_store());
|
||||
|
||||
let trusted_workspaces = HashSet::from_iter([None]);
|
||||
PROJECT_DB
|
||||
.save_trusted_worktrees(HashMap::default(), trusted_workspaces)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
PROJECT_DB.clear_trusted_worktrees().await.unwrap();
|
||||
|
||||
let fetched = cx.update(|cx| {
|
||||
PROJECT_DB.fetch_trusted_worktrees(Some(worktree_store.clone()), None, cx)
|
||||
});
|
||||
let fetched = fetched.unwrap();
|
||||
|
||||
assert!(fetched.is_empty(), "should be empty after clear");
|
||||
|
||||
let fetched_no_store = cx
|
||||
.update(|cx| PROJECT_DB.fetch_trusted_worktrees(None, None, cx))
|
||||
.unwrap();
|
||||
assert!(fetched_no_store.is_empty(), "should be empty after clear");
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ pub mod image_store;
|
|||
pub mod lsp_command;
|
||||
pub mod lsp_store;
|
||||
mod manifest_tree;
|
||||
mod persistence;
|
||||
pub mod prettier_store;
|
||||
mod project_search;
|
||||
pub mod project_settings;
|
||||
|
|
@ -19,6 +20,7 @@ pub mod task_store;
|
|||
pub mod telemetry_snapshot;
|
||||
pub mod terminals;
|
||||
pub mod toolchain_store;
|
||||
pub mod trusted_worktrees;
|
||||
pub mod worktree_store;
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -39,6 +41,7 @@ use crate::{
|
|||
git_store::GitStore,
|
||||
lsp_store::{SymbolLocation, log_store::LogKind},
|
||||
project_search::SearchResultsHandle,
|
||||
trusted_worktrees::{PathTrust, RemoteHostLocation, TrustedWorktrees},
|
||||
};
|
||||
pub use agent_server_store::{AgentServerStore, AgentServersUpdated, ExternalAgentServerName};
|
||||
pub use git_store::{
|
||||
|
|
@ -1069,6 +1072,7 @@ impl Project {
|
|||
languages: Arc<LanguageRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
env: Option<HashMap<String, String>>,
|
||||
init_worktree_trust: bool,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
cx.new(|cx: &mut Context<Self>| {
|
||||
|
|
@ -1077,6 +1081,15 @@ impl Project {
|
|||
.detach();
|
||||
let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([]), cx);
|
||||
let worktree_store = cx.new(|_| WorktreeStore::local(false, fs.clone()));
|
||||
if init_worktree_trust {
|
||||
trusted_worktrees::track_worktree_trust(
|
||||
worktree_store.clone(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
||||
.detach();
|
||||
|
||||
|
|
@ -1250,6 +1263,7 @@ impl Project {
|
|||
user_store: Entity<UserStore>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
init_worktree_trust: bool,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
cx.new(|cx: &mut Context<Self>| {
|
||||
|
|
@ -1258,8 +1272,14 @@ impl Project {
|
|||
.detach();
|
||||
let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([]), cx);
|
||||
|
||||
let (remote_proto, path_style) =
|
||||
remote.read_with(cx, |remote, _| (remote.proto_client(), remote.path_style()));
|
||||
let (remote_proto, path_style, connection_options) =
|
||||
remote.read_with(cx, |remote, _| {
|
||||
(
|
||||
remote.proto_client(),
|
||||
remote.path_style(),
|
||||
remote.connection_options(),
|
||||
)
|
||||
});
|
||||
let worktree_store = cx.new(|_| {
|
||||
WorktreeStore::remote(
|
||||
false,
|
||||
|
|
@ -1268,8 +1288,23 @@ impl Project {
|
|||
path_style,
|
||||
)
|
||||
});
|
||||
|
||||
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
||||
.detach();
|
||||
if init_worktree_trust {
|
||||
match &connection_options {
|
||||
RemoteConnectionOptions::Wsl(..) | RemoteConnectionOptions::Ssh(..) => {
|
||||
trusted_worktrees::track_worktree_trust(
|
||||
worktree_store.clone(),
|
||||
Some(RemoteHostLocation::from(connection_options)),
|
||||
None,
|
||||
Some((remote_proto.clone(), REMOTE_SERVER_PROJECT_ID)),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
RemoteConnectionOptions::Docker(..) => {}
|
||||
}
|
||||
}
|
||||
|
||||
let weak_self = cx.weak_entity();
|
||||
let context_server_store =
|
||||
|
|
@ -1450,6 +1485,9 @@ impl Project {
|
|||
remote_proto.add_entity_request_handler(Self::handle_language_server_prompt_request);
|
||||
remote_proto.add_entity_message_handler(Self::handle_hide_toast);
|
||||
remote_proto.add_entity_request_handler(Self::handle_update_buffer_from_remote_server);
|
||||
remote_proto.add_entity_request_handler(Self::handle_trust_worktrees);
|
||||
remote_proto.add_entity_request_handler(Self::handle_restrict_worktrees);
|
||||
|
||||
BufferStore::init(&remote_proto);
|
||||
LspStore::init(&remote_proto);
|
||||
SettingsObserver::init(&remote_proto);
|
||||
|
|
@ -1810,6 +1848,7 @@ impl Project {
|
|||
Arc::new(languages),
|
||||
fs,
|
||||
None,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
|
|
@ -1834,6 +1873,25 @@ impl Project {
|
|||
fs: Arc<dyn Fs>,
|
||||
root_paths: impl IntoIterator<Item = &Path>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) -> Entity<Project> {
|
||||
Self::test_project(fs, root_paths, false, cx).await
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn test_with_worktree_trust(
|
||||
fs: Arc<dyn Fs>,
|
||||
root_paths: impl IntoIterator<Item = &Path>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) -> Entity<Project> {
|
||||
Self::test_project(fs, root_paths, true, cx).await
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
async fn test_project(
|
||||
fs: Arc<dyn Fs>,
|
||||
root_paths: impl IntoIterator<Item = &Path>,
|
||||
init_worktree_trust: bool,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) -> Entity<Project> {
|
||||
use clock::FakeSystemClock;
|
||||
|
||||
|
|
@ -1850,6 +1908,7 @@ impl Project {
|
|||
Arc::new(languages),
|
||||
fs,
|
||||
None,
|
||||
init_worktree_trust,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
|
@ -4757,9 +4816,14 @@ impl Project {
|
|||
envelope: TypedEnvelope<proto::UpdateWorktree>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |project, cx| {
|
||||
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
|
||||
if let Some(worktree) = this.worktree_for_id(worktree_id, cx) {
|
||||
if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
|
||||
trusted_worktrees.update(cx, |trusted_worktrees, cx| {
|
||||
trusted_worktrees.can_trust(worktree_id, cx)
|
||||
});
|
||||
}
|
||||
if let Some(worktree) = project.worktree_for_id(worktree_id, cx) {
|
||||
worktree.update(cx, |worktree, _| {
|
||||
let worktree = worktree.as_remote_mut().unwrap();
|
||||
worktree.update_from_remote(envelope.payload);
|
||||
|
|
@ -4786,6 +4850,61 @@ impl Project {
|
|||
BufferStore::handle_update_buffer(buffer_store, envelope, cx).await
|
||||
}
|
||||
|
||||
async fn handle_trust_worktrees(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::TrustWorktrees>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::Ack> {
|
||||
let trusted_worktrees = cx
|
||||
.update(|cx| TrustedWorktrees::try_get_global(cx))?
|
||||
.context("missing trusted worktrees")?;
|
||||
trusted_worktrees.update(&mut cx, |trusted_worktrees, cx| {
|
||||
let remote_host = this
|
||||
.read(cx)
|
||||
.remote_connection_options(cx)
|
||||
.map(RemoteHostLocation::from);
|
||||
trusted_worktrees.trust(
|
||||
envelope
|
||||
.payload
|
||||
.trusted_paths
|
||||
.into_iter()
|
||||
.filter_map(|proto_path| PathTrust::from_proto(proto_path))
|
||||
.collect(),
|
||||
remote_host,
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
async fn handle_restrict_worktrees(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::RestrictWorktrees>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::Ack> {
|
||||
let trusted_worktrees = cx
|
||||
.update(|cx| TrustedWorktrees::try_get_global(cx))?
|
||||
.context("missing trusted worktrees")?;
|
||||
trusted_worktrees.update(&mut cx, |trusted_worktrees, cx| {
|
||||
let mut restricted_paths = envelope
|
||||
.payload
|
||||
.worktree_ids
|
||||
.into_iter()
|
||||
.map(WorktreeId::from_proto)
|
||||
.map(PathTrust::Worktree)
|
||||
.collect::<HashSet<_>>();
|
||||
if envelope.payload.restrict_workspace {
|
||||
restricted_paths.insert(PathTrust::Workspace);
|
||||
}
|
||||
let remote_host = this
|
||||
.read(cx)
|
||||
.remote_connection_options(cx)
|
||||
.map(RemoteHostLocation::from);
|
||||
trusted_worktrees.restrict(restricted_paths, remote_host, cx);
|
||||
})?;
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
async fn handle_update_buffer(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::UpdateBuffer>,
|
||||
|
|
|
|||
|
|
@ -23,13 +23,14 @@ 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};
|
||||
|
||||
use crate::{
|
||||
task_store::{TaskSettingsLocation, TaskStore},
|
||||
trusted_worktrees::{PathTrust, TrustedWorktrees, TrustedWorktreesEvent},
|
||||
worktree_store::{WorktreeStore, WorktreeStoreEvent},
|
||||
};
|
||||
|
||||
|
|
@ -83,6 +84,12 @@ pub struct SessionSettings {
|
|||
///
|
||||
/// Default: true
|
||||
pub restore_unsaved_buffers: bool,
|
||||
/// Whether or not to skip worktree trust checks.
|
||||
/// When trusted, project settings are synchronized automatically,
|
||||
/// language and MCP servers are downloaded and started automatically.
|
||||
///
|
||||
/// Default: false
|
||||
pub trust_all_worktrees: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
|
|
@ -570,6 +577,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 +603,9 @@ pub struct SettingsObserver {
|
|||
worktree_store: Entity<WorktreeStore>,
|
||||
project_id: u64,
|
||||
task_store: Entity<TaskStore>,
|
||||
pending_local_settings:
|
||||
HashMap<PathTrust, 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 +631,61 @@ impl SettingsObserver {
|
|||
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
||||
.detach();
|
||||
|
||||
let _trusted_worktrees_watcher =
|
||||
TrustedWorktrees::try_get_global(cx).map(|trusted_worktrees| {
|
||||
cx.subscribe(
|
||||
&trusted_worktrees,
|
||||
move |settings_observer, _, e, cx| match e {
|
||||
TrustedWorktreesEvent::Trusted(_, trusted_paths) => {
|
||||
for trusted_path in trusted_paths {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
TrustedWorktreesEvent::Restricted(..) => {}
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
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 +738,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(),
|
||||
|
|
@ -975,36 +1038,32 @@ 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 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 let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
|
||||
trusted_worktrees.update(cx, |trusted_worktrees, cx| {
|
||||
trusted_worktrees.can_trust(worktree_id, cx)
|
||||
})
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}),
|
||||
}) {
|
||||
apply_local_settings(worktree_id, &directory, kind, &file_content, cx)
|
||||
} else {
|
||||
applied = false;
|
||||
self.pending_local_settings
|
||||
.entry(PathTrust::Worktree(worktree_id))
|
||||
.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(
|
||||
|
|
@ -1067,16 +1126,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1193,6 +1254,37 @@ 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,
|
||||
|
|
|
|||
1933
crates/project/src/trusted_worktrees.rs
Normal file
1933
crates/project/src/trusted_worktrees.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -62,7 +62,7 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
let client = Client::production(cx);
|
||||
let http_client = FakeHttpClient::with_200_response();
|
||||
let (_, rx) = watch::channel(None);
|
||||
let node = NodeRuntime::new(http_client, None, rx);
|
||||
let node = NodeRuntime::new(http_client, None, rx, None);
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
let registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
|
||||
let fs = Arc::new(RealFs::new(None, cx.background_executor().clone()));
|
||||
|
|
@ -73,6 +73,7 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
registry,
|
||||
fs,
|
||||
Some(Default::default()),
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -158,3 +158,22 @@ message UpdateUserSettings {
|
|||
uint64 project_id = 1;
|
||||
string contents = 2;
|
||||
}
|
||||
|
||||
message TrustWorktrees {
|
||||
uint64 project_id = 1;
|
||||
repeated PathTrust trusted_paths = 2;
|
||||
}
|
||||
|
||||
message PathTrust {
|
||||
oneof content {
|
||||
uint64 workspace = 1;
|
||||
uint64 worktree_id = 2;
|
||||
string abs_path = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message RestrictWorktrees {
|
||||
uint64 project_id = 1;
|
||||
bool restrict_workspace = 2;
|
||||
repeated uint64 worktree_ids = 3;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -448,7 +448,10 @@ message Envelope {
|
|||
ExternalExtensionAgentsUpdated external_extension_agents_updated = 401;
|
||||
|
||||
GitCreateRemote git_create_remote = 402;
|
||||
GitRemoveRemote git_remove_remote = 403;// current max
|
||||
GitRemoveRemote git_remove_remote = 403;
|
||||
|
||||
TrustWorktrees trust_worktrees = 404;
|
||||
RestrictWorktrees restrict_worktrees = 405; // current max
|
||||
}
|
||||
|
||||
reserved 87 to 88, 396;
|
||||
|
|
|
|||
|
|
@ -310,6 +310,8 @@ messages!(
|
|||
(GitCreateBranch, Background),
|
||||
(GitChangeBranch, Background),
|
||||
(GitRenameBranch, Background),
|
||||
(TrustWorktrees, Background),
|
||||
(RestrictWorktrees, Background),
|
||||
(CheckForPushedCommits, Background),
|
||||
(CheckForPushedCommitsResponse, Background),
|
||||
(GitDiff, Background),
|
||||
|
|
@ -529,7 +531,9 @@ request_messages!(
|
|||
(GetAgentServerCommand, AgentServerCommand),
|
||||
(RemoteStarted, Ack),
|
||||
(GitGetWorktrees, GitWorktreesResponse),
|
||||
(GitCreateWorktree, Ack)
|
||||
(GitCreateWorktree, Ack),
|
||||
(TrustWorktrees, Ack),
|
||||
(RestrictWorktrees, Ack),
|
||||
);
|
||||
|
||||
lsp_messages!(
|
||||
|
|
@ -702,7 +706,9 @@ entity_messages!(
|
|||
ExternalAgentLoadingStatusUpdated,
|
||||
NewExternalAgentVersionAvailable,
|
||||
GitGetWorktrees,
|
||||
GitCreateWorktree
|
||||
GitCreateWorktree,
|
||||
TrustWorktrees,
|
||||
RestrictWorktrees,
|
||||
);
|
||||
|
||||
entity_messages!(
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ use gpui::{
|
|||
|
||||
use language::{CursorShape, Point};
|
||||
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
|
||||
use project::trusted_worktrees;
|
||||
use release_channel::ReleaseChannel;
|
||||
use remote::{
|
||||
ConnectionIdentifier, DockerConnectionOptions, RemoteClient, RemoteConnection,
|
||||
|
|
@ -646,6 +647,7 @@ pub async fn open_remote_project(
|
|||
app_state.languages.clone(),
|
||||
app_state.fs.clone(),
|
||||
None,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
cx.new(|cx| {
|
||||
|
|
@ -788,11 +790,20 @@ pub async fn open_remote_project(
|
|||
continue;
|
||||
}
|
||||
|
||||
if created_new_window {
|
||||
window
|
||||
.update(cx, |_, window, _| window.remove_window())
|
||||
.ok();
|
||||
}
|
||||
window
|
||||
.update(cx, |workspace, window, cx| {
|
||||
if created_new_window {
|
||||
window.remove_window();
|
||||
}
|
||||
trusted_worktrees::track_worktree_trust(
|
||||
workspace.project().read(cx).worktree_store(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
Ok(items) => {
|
||||
|
|
|
|||
|
|
@ -1000,6 +1000,7 @@ impl RemoteServerProjects {
|
|||
app_state.user_store.clone(),
|
||||
app_state.languages.clone(),
|
||||
app_state.fs.clone(),
|
||||
true,
|
||||
cx,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ anyhow.workspace = true
|
|||
askpass.workspace = true
|
||||
clap.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
dap_adapters.workspace = true
|
||||
debug_adapter_extension.workspace = true
|
||||
env_logger.workspace = true
|
||||
|
|
@ -81,7 +82,6 @@ action_log.workspace = true
|
|||
agent = { workspace = true, features = ["test-support"] }
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
collections.workspace = true
|
||||
dap = { workspace = true, features = ["test-support"] }
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use anyhow::{Context as _, Result, anyhow};
|
||||
use collections::HashSet;
|
||||
use language::File;
|
||||
use lsp::LanguageServerId;
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ use project::{
|
|||
project_settings::SettingsObserver,
|
||||
search::SearchQuery,
|
||||
task_store::TaskStore,
|
||||
trusted_worktrees::{PathTrust, RemoteHostLocation, TrustedWorktrees},
|
||||
worktree_store::WorktreeStore,
|
||||
};
|
||||
use rpc::{
|
||||
|
|
@ -86,6 +88,7 @@ impl HeadlessProject {
|
|||
languages,
|
||||
extension_host_proxy: proxy,
|
||||
}: HeadlessAppState,
|
||||
init_worktree_trust: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
debug_adapter_extension::init(proxy.clone(), cx);
|
||||
|
|
@ -97,6 +100,16 @@ impl HeadlessProject {
|
|||
store
|
||||
});
|
||||
|
||||
if init_worktree_trust {
|
||||
project::trusted_worktrees::track_worktree_trust(
|
||||
worktree_store.clone(),
|
||||
None::<RemoteHostLocation>,
|
||||
Some((session.clone(), REMOTE_SERVER_PROJECT_ID)),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
let environment =
|
||||
cx.new(|cx| ProjectEnvironment::new(None, worktree_store.downgrade(), None, true, cx));
|
||||
let manifest_tree = ManifestTree::new(worktree_store.clone(), cx);
|
||||
|
|
@ -264,6 +277,8 @@ impl HeadlessProject {
|
|||
session.add_entity_request_handler(Self::handle_get_directory_environment);
|
||||
session.add_entity_message_handler(Self::handle_toggle_lsp_logs);
|
||||
session.add_entity_request_handler(Self::handle_open_image_by_path);
|
||||
session.add_entity_request_handler(Self::handle_trust_worktrees);
|
||||
session.add_entity_request_handler(Self::handle_restrict_worktrees);
|
||||
|
||||
session.add_entity_request_handler(BufferStore::handle_update_buffer);
|
||||
session.add_entity_message_handler(BufferStore::handle_close_buffer);
|
||||
|
|
@ -595,6 +610,53 @@ impl HeadlessProject {
|
|||
})
|
||||
}
|
||||
|
||||
pub async fn handle_trust_worktrees(
|
||||
_: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::TrustWorktrees>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::Ack> {
|
||||
let trusted_worktrees = cx
|
||||
.update(|cx| TrustedWorktrees::try_get_global(cx))?
|
||||
.context("missing trusted worktrees")?;
|
||||
trusted_worktrees.update(&mut cx, |trusted_worktrees, cx| {
|
||||
trusted_worktrees.trust(
|
||||
envelope
|
||||
.payload
|
||||
.trusted_paths
|
||||
.into_iter()
|
||||
.filter_map(PathTrust::from_proto)
|
||||
.collect(),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
pub async fn handle_restrict_worktrees(
|
||||
_: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::RestrictWorktrees>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::Ack> {
|
||||
let trusted_worktrees = cx
|
||||
.update(|cx| TrustedWorktrees::try_get_global(cx))?
|
||||
.context("missing trusted worktrees")?;
|
||||
trusted_worktrees.update(&mut cx, |trusted_worktrees, cx| {
|
||||
let mut restricted_paths = envelope
|
||||
.payload
|
||||
.worktree_ids
|
||||
.into_iter()
|
||||
.map(WorktreeId::from_proto)
|
||||
.map(PathTrust::Worktree)
|
||||
.collect::<HashSet<_>>();
|
||||
if envelope.payload.restrict_workspace {
|
||||
restricted_paths.insert(PathTrust::Workspace);
|
||||
}
|
||||
trusted_worktrees.restrict(restricted_paths, None, cx);
|
||||
})?;
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
pub async fn handle_open_new_buffer(
|
||||
this: Entity<Self>,
|
||||
_message: TypedEnvelope<proto::OpenNewBuffer>,
|
||||
|
|
|
|||
|
|
@ -1933,6 +1933,7 @@ pub async fn init_test(
|
|||
languages,
|
||||
extension_host_proxy: proxy,
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
|
@ -1977,5 +1978,5 @@ fn build_project(ssh: Entity<RemoteClient>, cx: &mut TestAppContext) -> Entity<P
|
|||
Project::init(&client, cx);
|
||||
});
|
||||
|
||||
cx.update(|cx| Project::remote(ssh, client, node, user_store, languages, fs, cx))
|
||||
cx.update(|cx| Project::remote(ssh, client, node, user_store, languages, fs, false, cx))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use crate::HeadlessProject;
|
|||
use crate::headless_project::HeadlessAppState;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use client::ProxySettings;
|
||||
use project::trusted_worktrees;
|
||||
use util::ResultExt;
|
||||
|
||||
use extension::ExtensionHostProxy;
|
||||
|
|
@ -34,6 +35,7 @@ use smol::Async;
|
|||
use smol::channel::{Receiver, Sender};
|
||||
use smol::io::AsyncReadExt;
|
||||
use smol::{net::unix::UnixListener, stream::StreamExt as _};
|
||||
use std::pin::Pin;
|
||||
use std::{
|
||||
env,
|
||||
ffi::OsStr,
|
||||
|
|
@ -417,6 +419,7 @@ pub fn execute_run(
|
|||
|
||||
log::info!("gpui app started, initializing server");
|
||||
let session = start_server(listeners, log_rx, cx, is_wsl_interop);
|
||||
trusted_worktrees::init(Some((session.clone(), REMOTE_SERVER_PROJECT_ID)), None, cx);
|
||||
|
||||
GitHostingProviderRegistry::set_global(git_hosting_provider_registry, cx);
|
||||
git_hosting_providers::init(cx);
|
||||
|
|
@ -449,10 +452,13 @@ pub fn execute_run(
|
|||
)
|
||||
};
|
||||
|
||||
let trust_task = trusted_worktrees::wait_for_default_workspace_trust("Node runtime", cx)
|
||||
.map(|trust_task| Box::pin(trust_task) as Pin<Box<_>>);
|
||||
let node_runtime = NodeRuntime::new(
|
||||
http_client.clone(),
|
||||
Some(shell_env_loaded_rx),
|
||||
node_settings_rx,
|
||||
trust_task,
|
||||
);
|
||||
|
||||
let mut languages = LanguageRegistry::new(cx.background_executor().clone());
|
||||
|
|
@ -468,6 +474,7 @@ pub fn execute_run(
|
|||
languages,
|
||||
extension_host_proxy,
|
||||
},
|
||||
true,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
|
|
|||
|
|
@ -187,6 +187,12 @@ pub struct SessionSettingsContent {
|
|||
///
|
||||
/// Default: true
|
||||
pub restore_unsaved_buffers: Option<bool>,
|
||||
/// Whether or not to skip worktree trust checks.
|
||||
/// When trusted, project settings are synchronized automatically,
|
||||
/// language and MCP servers are downloaded and started automatically.
|
||||
///
|
||||
/// Default: false
|
||||
pub trust_all_worktrees: Option<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 Projects By Default",
|
||||
description: "When opening Zed, avoid Restricted Mode by auto-trusting all projects, enabling use of all features without having to give permission to each new project.",
|
||||
field: Box::new(SettingField {
|
||||
json_path: Some("session.trust_all_projects"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.session
|
||||
.as_ref()
|
||||
.and_then(|session| session.trust_all_worktrees.as_ref())
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content
|
||||
.session
|
||||
.get_or_insert_default()
|
||||
.trust_all_worktrees = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SectionHeader("Workspace Restoration"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Restore Unsaved Buffers",
|
||||
|
|
|
|||
|
|
@ -30,18 +30,20 @@ use gpui::{
|
|||
Subscription, WeakEntity, Window, actions, div,
|
||||
};
|
||||
use onboarding_banner::OnboardingBanner;
|
||||
use project::{Project, WorktreeSettings, git_store::GitStoreEvent};
|
||||
use project::{
|
||||
Project, WorktreeSettings, git_store::GitStoreEvent, trusted_worktrees::TrustedWorktrees,
|
||||
};
|
||||
use remote::RemoteConnectionOptions;
|
||||
use settings::{Settings, SettingsLocation};
|
||||
use std::sync::Arc;
|
||||
use theme::ActiveTheme;
|
||||
use title_bar_settings::TitleBarSettings;
|
||||
use ui::{
|
||||
Avatar, Button, ButtonLike, ButtonStyle, Chip, ContextMenu, Icon, IconName, IconSize,
|
||||
IconWithIndicator, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, h_flex, prelude::*,
|
||||
Avatar, ButtonLike, Chip, ContextMenu, IconWithIndicator, Indicator, PopoverMenu,
|
||||
PopoverMenuHandle, TintColor, Tooltip, prelude::*,
|
||||
};
|
||||
use util::{ResultExt, rel_path::RelPath};
|
||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
use workspace::{ToggleWorktreeSecurity, Workspace, notifications::NotifyResultExt};
|
||||
use zed_actions::{OpenRecent, OpenRemote};
|
||||
|
||||
pub use onboarding_banner::restore_banner;
|
||||
|
|
@ -163,6 +165,7 @@ impl Render for TitleBar {
|
|||
title_bar
|
||||
.when(title_bar_settings.show_project_items, |title_bar| {
|
||||
title_bar
|
||||
.children(self.render_restricted_mode(cx))
|
||||
.children(self.render_project_host(cx))
|
||||
.child(self.render_project_name(cx))
|
||||
})
|
||||
|
|
@ -291,7 +294,12 @@ impl TitleBar {
|
|||
_ => {}
|
||||
}),
|
||||
);
|
||||
subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
|
||||
subscriptions.push(cx.observe(&user_store, |_a, _, cx| cx.notify()));
|
||||
if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
|
||||
subscriptions.push(cx.subscribe(&trusted_worktrees, |_, _, _, cx| {
|
||||
cx.notify();
|
||||
}));
|
||||
}
|
||||
|
||||
let banner = cx.new(|cx| {
|
||||
OnboardingBanner::new(
|
||||
|
|
@ -317,7 +325,7 @@ impl TitleBar {
|
|||
client,
|
||||
_subscriptions: subscriptions,
|
||||
banner,
|
||||
screen_share_popover_handle: Default::default(),
|
||||
screen_share_popover_handle: PopoverMenuHandle::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -427,6 +435,48 @@ impl TitleBar {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn render_restricted_mode(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let has_restricted_worktrees = TrustedWorktrees::try_get_global(cx)
|
||||
.map(|trusted_worktrees| {
|
||||
trusted_worktrees
|
||||
.read(cx)
|
||||
.has_restricted_worktrees(&self.project.read(cx).worktree_store(), cx)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if !has_restricted_worktrees {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
Button::new("restricted_mode_trigger", "Restricted Mode")
|
||||
.style(ButtonStyle::Tinted(TintColor::Warning))
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Warning)
|
||||
.icon(IconName::Warning)
|
||||
.icon_color(Color::Warning)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
.tooltip(|_, cx| {
|
||||
Tooltip::with_meta(
|
||||
"You're in Restricted Mode",
|
||||
Some(&ToggleWorktreeSecurity),
|
||||
"Mark this project as trusted and unlock all features",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.show_worktree_trust_security_modal(true, window, cx)
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
})
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn render_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
if self.project.read(cx).is_via_remote_server() {
|
||||
return self.render_remote_project_connection(cx);
|
||||
|
|
|
|||
|
|
@ -1,73 +1,161 @@
|
|||
use crate::component_prelude::*;
|
||||
use crate::prelude::*;
|
||||
use crate::{Checkbox, ListBulletItem, ToggleState};
|
||||
use gpui::Action;
|
||||
use gpui::FocusHandle;
|
||||
use gpui::IntoElement;
|
||||
use gpui::Stateful;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
type ActionHandler = Box<dyn FnOnce(Stateful<Div>) -> Stateful<Div>>;
|
||||
|
||||
#[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>,
|
||||
key_context: Option<String>,
|
||||
action_handlers: Vec<ActionHandler>,
|
||||
focus_handle: Option<FocusHandle>,
|
||||
}
|
||||
|
||||
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,
|
||||
key_context: None,
|
||||
action_handlers: Vec::new(),
|
||||
focus_handle: 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
|
||||
}
|
||||
|
||||
pub fn key_context(mut self, key_context: impl Into<String>) -> Self {
|
||||
self.key_context = Some(key_context.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_action<A: Action>(
|
||||
mut self,
|
||||
listener: impl Fn(&A, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.action_handlers
|
||||
.push(Box::new(move |div| div.on_action(listener)));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self {
|
||||
self.focus_handle = Some(focus_handle.clone());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AlertModal {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
v_flex()
|
||||
let width = self.width.unwrap_or_else(|| px(440.).into());
|
||||
let has_default_footer = self.primary_action.is_some() || self.dismiss_label.is_some();
|
||||
|
||||
let mut modal = v_flex()
|
||||
.when_some(self.key_context, |this, key_context| {
|
||||
this.key_context(key_context.as_str())
|
||||
})
|
||||
.when_some(self.focus_handle, |this, focus_handle| {
|
||||
this.track_focus(&focus_handle)
|
||||
})
|
||||
.id(self.id)
|
||||
.elevation_3(cx)
|
||||
.w(px(440.))
|
||||
.p_5()
|
||||
.child(
|
||||
.w(width)
|
||||
.bg(cx.theme().colors().elevated_surface_background)
|
||||
.overflow_hidden();
|
||||
|
||||
for handler in self.action_handlers {
|
||||
modal = handler(modal);
|
||||
}
|
||||
|
||||
if let Some(header) = self.header {
|
||||
modal = modal.child(header);
|
||||
} else if let Some(title) = self.title {
|
||||
modal = modal.child(
|
||||
v_flex()
|
||||
.pt_3()
|
||||
.pr_3()
|
||||
.pl_3()
|
||||
.pb_1()
|
||||
.child(Headline::new(title).size(HeadlineSize::Small)),
|
||||
);
|
||||
}
|
||||
|
||||
if !self.children.is_empty() {
|
||||
modal = modal.child(
|
||||
v_flex()
|
||||
.p_3()
|
||||
.text_ui(cx)
|
||||
.text_color(Color::Muted.color(cx))
|
||||
.gap_1()
|
||||
.child(Headline::new(self.title).size(HeadlineSize::Small))
|
||||
.children(self.children),
|
||||
)
|
||||
.child(
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(footer) = self.footer {
|
||||
modal = modal.child(footer);
|
||||
} else if has_default_footer {
|
||||
let primary_action = self.primary_action.unwrap_or_else(|| "Ok".into());
|
||||
let dismiss_label = self.dismiss_label.unwrap_or_else(|| "Cancel".into());
|
||||
|
||||
modal = modal.child(
|
||||
h_flex()
|
||||
.h(rems(1.75))
|
||||
.p_3()
|
||||
.items_center()
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new(self.dismiss_label.clone(), self.dismiss_label.clone())
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Button::new(
|
||||
self.primary_action.clone(),
|
||||
self.primary_action,
|
||||
)),
|
||||
),
|
||||
)
|
||||
.justify_end()
|
||||
.gap_1()
|
||||
.child(Button::new(dismiss_label.clone(), dismiss_label).color(Color::Muted))
|
||||
.child(Button::new(primary_action.clone(), primary_action)),
|
||||
);
|
||||
}
|
||||
|
||||
modal
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,24 +178,75 @@ impl Component for AlertModal {
|
|||
Some("A modal dialog that presents an alert message with primary and dismiss actions.")
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<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", "Stay in Restricted Mode").color(Color::Muted))
|
||||
.child(Button::new("trust", "Trust and Continue").style(ButtonStyle::Filled))
|
||||
)
|
||||
)
|
||||
.width(rems(40.))
|
||||
.into_any_element(),
|
||||
)]),
|
||||
])
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -171,28 +171,19 @@ impl Render for ModalLayer {
|
|||
};
|
||||
|
||||
div()
|
||||
.occlude()
|
||||
.absolute()
|
||||
.size_full()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.when(active_modal.modal.fade_out_background(cx), |el| {
|
||||
.inset_0()
|
||||
.occlude()
|
||||
.when(active_modal.modal.fade_out_background(cx), |this| {
|
||||
let mut background = cx.theme().colors().elevated_surface_background;
|
||||
background.fade_out(0.2);
|
||||
el.bg(background)
|
||||
this.bg(background)
|
||||
})
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.hide_modal(window, cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.h(px(0.0))
|
||||
.top_20()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.track_focus(&active_modal.focus_handle)
|
||||
.child(
|
||||
|
|
|
|||
373
crates/workspace/src/security_modal.rs
Normal file
373
crates/workspace/src/security_modal.rs
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
//! A UI interface for managing the [`TrustedWorktrees`] data.
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use collections::{HashMap, HashSet};
|
||||
use gpui::{DismissEvent, EventEmitter, FocusHandle, Focusable, WeakEntity};
|
||||
|
||||
use project::{
|
||||
WorktreeId,
|
||||
trusted_worktrees::{PathTrust, RemoteHostLocation, TrustedWorktrees},
|
||||
worktree_store::WorktreeStore,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
AlertModal, Checkbox, FluentBuilder, KeyBinding, ListBulletItem, ToggleState, prelude::*,
|
||||
};
|
||||
|
||||
use crate::{DismissDecision, ModalView, ToggleWorktreeSecurity};
|
||||
|
||||
pub struct SecurityModal {
|
||||
restricted_paths: HashMap<Option<WorktreeId>, RestrictedPath>,
|
||||
home_dir: Option<PathBuf>,
|
||||
trust_parents: bool,
|
||||
worktree_store: WeakEntity<WorktreeStore>,
|
||||
remote_host: Option<RemoteHostLocation>,
|
||||
focus_handle: FocusHandle,
|
||||
trusted: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct RestrictedPath {
|
||||
abs_path: Option<Arc<Path>>,
|
||||
is_file: bool,
|
||||
host: Option<RemoteHostLocation>,
|
||||
}
|
||||
|
||||
impl Focusable for SecurityModal {
|
||||
fn focus_handle(&self, _: &ui::App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for SecurityModal {}
|
||||
|
||||
impl ModalView for SecurityModal {
|
||||
fn fade_out_background(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_before_dismiss(&mut self, _: &mut Window, _: &mut Context<Self>) -> DismissDecision {
|
||||
match self.trusted {
|
||||
Some(false) => telemetry::event!("Open in Restricted", source = "Worktree Trust Modal"),
|
||||
Some(true) => telemetry::event!("Trust and Continue", source = "Worktree Trust Modal"),
|
||||
None => telemetry::event!("Dismissed", source = "Worktree Trust Modal"),
|
||||
}
|
||||
DismissDecision::Dismiss(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SecurityModal {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if self.restricted_paths.is_empty() {
|
||||
self.dismiss(cx);
|
||||
return v_flex().into_any_element();
|
||||
}
|
||||
|
||||
let header_label = if self.restricted_paths.len() == 1 {
|
||||
"Unrecognized Project"
|
||||
} else {
|
||||
"Unrecognized Projects"
|
||||
};
|
||||
|
||||
let trust_label = self.build_trust_label();
|
||||
|
||||
AlertModal::new("security-modal")
|
||||
.width(rems(40.))
|
||||
.key_context("SecurityModal")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.on_action(cx.listener(|this, _: &menu::Confirm, _window, cx| {
|
||||
this.trust_and_dismiss(cx);
|
||||
}))
|
||||
.on_action(cx.listener(|security_modal, _: &ToggleWorktreeSecurity, _window, cx| {
|
||||
security_modal.trusted = Some(false);
|
||||
security_modal.dismiss(cx);
|
||||
}))
|
||||
.header(
|
||||
v_flex()
|
||||
.p_3()
|
||||
.gap_1()
|
||||
.rounded_t_md()
|
||||
.bg(cx.theme().colors().editor_background.opacity(0.5))
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::Warning).color(Color::Warning))
|
||||
.child(Label::new(header_label)),
|
||||
)
|
||||
.children(self.restricted_paths.values().map(|restricted_path| {
|
||||
let abs_path = restricted_path.abs_path.as_ref().and_then(|abs_path| {
|
||||
if restricted_path.is_file {
|
||||
abs_path.parent()
|
||||
} else {
|
||||
Some(abs_path.as_ref())
|
||||
}
|
||||
});
|
||||
|
||||
let label = match abs_path {
|
||||
Some(abs_path) => match &restricted_path.host {
|
||||
Some(remote_host) => match &remote_host.user_name {
|
||||
Some(user_name) => format!(
|
||||
"{} ({}@{})",
|
||||
self.shorten_path(abs_path).display(),
|
||||
user_name,
|
||||
remote_host.host_identifier
|
||||
),
|
||||
None => format!(
|
||||
"{} ({})",
|
||||
self.shorten_path(abs_path).display(),
|
||||
remote_host.host_identifier
|
||||
),
|
||||
},
|
||||
None => self.shorten_path(abs_path).display().to_string(),
|
||||
},
|
||||
None => match &restricted_path.host {
|
||||
Some(remote_host) => match &remote_host.user_name {
|
||||
Some(user_name) => format!(
|
||||
"Empty project ({}@{})",
|
||||
user_name, remote_host.host_identifier
|
||||
),
|
||||
None => {
|
||||
format!("Empty project ({})", remote_host.host_identifier)
|
||||
}
|
||||
},
|
||||
None => "Empty project".to_string(),
|
||||
},
|
||||
};
|
||||
h_flex()
|
||||
.pl(IconSize::default().rems() + rems(0.5))
|
||||
.child(Label::new(label).color(Color::Muted))
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
v_flex()
|
||||
.child(
|
||||
Label::new(
|
||||
"Untrusted projects are opened in Restricted Mode to protect your system.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new(
|
||||
"Review .zed/settings.json for any extensions or commands configured by this project.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.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 Server integrations from installing")),
|
||||
)
|
||||
.map(|this| match trust_label {
|
||||
Some(trust_label) => this.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();
|
||||
cx.stop_propagation();
|
||||
},
|
||||
)),
|
||||
),
|
||||
None => this,
|
||||
}),
|
||||
)
|
||||
.footer(
|
||||
h_flex()
|
||||
.px_3()
|
||||
.pb_3()
|
||||
.gap_1()
|
||||
.justify_end()
|
||||
.child(
|
||||
Button::new("rm", "Stay in Restricted Mode")
|
||||
.key_binding(
|
||||
KeyBinding::for_action(
|
||||
&ToggleWorktreeSecurity,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(cx.listener(move |security_modal, _, _, cx| {
|
||||
security_modal.trusted = Some(false);
|
||||
security_modal.dismiss(cx);
|
||||
cx.stop_propagation();
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("tc", "Trust and Continue")
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ui::ElevationIndex::ModalSurface)
|
||||
.key_binding(
|
||||
KeyBinding::for_action(&menu::Confirm, cx)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(cx.listener(move |security_modal, _, _, cx| {
|
||||
security_modal.trust_and_dismiss(cx);
|
||||
cx.stop_propagation();
|
||||
})),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl SecurityModal {
|
||||
pub fn new(
|
||||
worktree_store: WeakEntity<WorktreeStore>,
|
||||
remote_host: Option<impl Into<RemoteHostLocation>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let mut this = Self {
|
||||
worktree_store,
|
||||
remote_host: remote_host.map(|host| host.into()),
|
||||
restricted_paths: HashMap::default(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
trust_parents: false,
|
||||
home_dir: std::env::home_dir(),
|
||||
trusted: None,
|
||||
};
|
||||
this.refresh_restricted_paths(cx);
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
fn build_trust_label(&self) -> Option<Cow<'static, str>> {
|
||||
let mut has_restricted_files = false;
|
||||
let available_parents = self
|
||||
.restricted_paths
|
||||
.values()
|
||||
.filter(|restricted_path| {
|
||||
has_restricted_files |= restricted_path.is_file;
|
||||
!restricted_path.is_file
|
||||
})
|
||||
.filter_map(|restricted_path| restricted_path.abs_path.as_ref()?.parent())
|
||||
.collect::<SmallVec<[_; 2]>>();
|
||||
match available_parents.len() {
|
||||
0 => {
|
||||
if has_restricted_files {
|
||||
Some(Cow::Borrowed("Trust all single files"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
1 => Some(Cow::Owned(format!(
|
||||
"Trust all projects in the {:?} folder",
|
||||
self.shorten_path(available_parents[0])
|
||||
))),
|
||||
_ => Some(Cow::Borrowed("Trust all projects in the parent folders")),
|
||||
}
|
||||
}
|
||||
|
||||
fn shorten_path<'a>(&self, path: &'a Path) -> Cow<'a, 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),
|
||||
}
|
||||
}
|
||||
|
||||
fn trust_and_dismiss(&mut self, cx: &mut Context<Self>) {
|
||||
if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
|
||||
trusted_worktrees.update(cx, |trusted_worktrees, cx| {
|
||||
let mut paths_to_trust = self
|
||||
.restricted_paths
|
||||
.keys()
|
||||
.map(|worktree_id| match worktree_id {
|
||||
Some(worktree_id) => PathTrust::Worktree(*worktree_id),
|
||||
None => PathTrust::Workspace,
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
if self.trust_parents {
|
||||
paths_to_trust.extend(self.restricted_paths.values().filter_map(
|
||||
|restricted_paths| {
|
||||
if restricted_paths.is_file {
|
||||
Some(PathTrust::Workspace)
|
||||
} else {
|
||||
let parent_abs_path =
|
||||
restricted_paths.abs_path.as_ref()?.parent()?.to_owned();
|
||||
Some(PathTrust::AbsPath(parent_abs_path))
|
||||
}
|
||||
},
|
||||
));
|
||||
}
|
||||
trusted_worktrees.trust(paths_to_trust, self.remote_host.clone(), cx);
|
||||
});
|
||||
}
|
||||
|
||||
self.trusted = Some(true);
|
||||
self.dismiss(cx);
|
||||
}
|
||||
|
||||
pub fn dismiss(&mut self, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
pub fn refresh_restricted_paths(&mut self, cx: &mut Context<Self>) {
|
||||
if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
|
||||
if let Some(worktree_store) = self.worktree_store.upgrade() {
|
||||
let mut new_restricted_worktrees = trusted_worktrees
|
||||
.read(cx)
|
||||
.restricted_worktrees(worktree_store.read(cx), self.remote_host.clone(), cx)
|
||||
.into_iter()
|
||||
.filter_map(|restricted_path| {
|
||||
let restricted_path = match restricted_path {
|
||||
Some((worktree_id, abs_path)) => {
|
||||
let worktree =
|
||||
worktree_store.read(cx).worktree_for_id(worktree_id, cx)?;
|
||||
(
|
||||
Some(worktree_id),
|
||||
RestrictedPath {
|
||||
abs_path: Some(abs_path),
|
||||
is_file: worktree.read(cx).is_single_file(),
|
||||
host: self.remote_host.clone(),
|
||||
},
|
||||
)
|
||||
}
|
||||
None => (
|
||||
None,
|
||||
RestrictedPath {
|
||||
abs_path: None,
|
||||
is_file: false,
|
||||
host: self.remote_host.clone(),
|
||||
},
|
||||
),
|
||||
};
|
||||
Some(restricted_path)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
// Do not clutter the UI:
|
||||
// * trusting regular local worktrees assumes the workspace is trusted either, on the same host.
|
||||
// * trusting a workspace trusts all single-file worktrees on the same host.
|
||||
if new_restricted_worktrees.len() > 1 {
|
||||
new_restricted_worktrees.remove(&None);
|
||||
}
|
||||
|
||||
if self.restricted_paths != new_restricted_worktrees {
|
||||
self.trust_parents = false;
|
||||
self.restricted_paths = new_restricted_worktrees;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
} else if !self.restricted_paths.is_empty() {
|
||||
self.restricted_paths.clear();
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -77,7 +78,9 @@ use project::{
|
|||
DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
|
||||
WorktreeSettings,
|
||||
debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
|
||||
project_settings::ProjectSettings,
|
||||
toolchain_store::ToolchainStoreEvent,
|
||||
trusted_worktrees::TrustedWorktrees,
|
||||
};
|
||||
use remote::{
|
||||
RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions,
|
||||
|
|
@ -86,7 +89,9 @@ use remote::{
|
|||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use session::AppSession;
|
||||
use settings::{CenteredPaddingSettings, Settings, SettingsLocation, update_settings_file};
|
||||
use settings::{
|
||||
CenteredPaddingSettings, Settings, SettingsLocation, SettingsStore, update_settings_file,
|
||||
};
|
||||
use shared_screen::SharedScreen;
|
||||
use sqlez::{
|
||||
bindable::{Bind, Column, StaticColumnCount},
|
||||
|
|
@ -137,6 +142,7 @@ use crate::{
|
|||
SerializedAxis,
|
||||
model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
|
||||
},
|
||||
security_modal::SecurityModal,
|
||||
utility_pane::{DraggedUtilityPane, UtilityPaneFrame, UtilityPaneSlot, UtilityPaneState},
|
||||
};
|
||||
|
||||
|
|
@ -277,6 +283,12 @@ actions!(
|
|||
ZoomIn,
|
||||
/// Zooms out of the active pane.
|
||||
ZoomOut,
|
||||
/// If any worktrees are in restricted mode, shows a modal with possible actions.
|
||||
/// If the modal is shown already, closes it without trusting any worktree.
|
||||
ToggleWorktreeSecurity,
|
||||
/// Clears all trusted worktrees, placing them in restricted mode on next open.
|
||||
/// Requires restart to take effect on already opened projects.
|
||||
ClearTrustedWorktrees,
|
||||
/// Stops following a collaborator.
|
||||
Unfollow,
|
||||
/// Restores the banner.
|
||||
|
|
@ -1217,6 +1229,17 @@ impl Workspace {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
cx.observe_global::<SettingsStore>(|_, cx| {
|
||||
if ProjectSettings::get_global(cx).session.trust_all_worktrees {
|
||||
if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
|
||||
trusted_worktrees.update(cx, |trusted_worktrees, cx| {
|
||||
trusted_worktrees.auto_trust_all(cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.subscribe_in(&project, window, move |this, _, event, window, cx| {
|
||||
match event {
|
||||
project::Event::RemoteIdChanged(_) => {
|
||||
|
|
@ -1474,7 +1497,7 @@ impl Workspace {
|
|||
}),
|
||||
];
|
||||
|
||||
cx.defer_in(window, |this, window, cx| {
|
||||
cx.defer_in(window, move |this, window, cx| {
|
||||
this.update_window_title(window, cx);
|
||||
this.show_initial_notifications(cx);
|
||||
});
|
||||
|
|
@ -1559,6 +1582,7 @@ impl Workspace {
|
|||
app_state.languages.clone(),
|
||||
app_state.fs.clone(),
|
||||
env,
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
|
||||
|
|
@ -5938,6 +5962,25 @@ impl Workspace {
|
|||
}
|
||||
},
|
||||
))
|
||||
.on_action(cx.listener(
|
||||
|workspace: &mut Workspace, _: &ToggleWorktreeSecurity, window, cx| {
|
||||
workspace.show_worktree_trust_security_modal(true, window, cx);
|
||||
},
|
||||
))
|
||||
.on_action(
|
||||
cx.listener(|_: &mut Workspace, _: &ClearTrustedWorktrees, _, cx| {
|
||||
if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
|
||||
let clear_task = trusted_worktrees.update(cx, |trusted_worktrees, cx| {
|
||||
trusted_worktrees.clear_trusted_paths(cx)
|
||||
});
|
||||
cx.spawn(async move |_, cx| {
|
||||
clear_task.await;
|
||||
cx.update(|cx| reload(cx)).ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}),
|
||||
)
|
||||
.on_action(cx.listener(
|
||||
|workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
|
||||
workspace.reopen_closed_item(window, cx).detach();
|
||||
|
|
@ -6418,6 +6461,41 @@ impl Workspace {
|
|||
file.project.all_languages.defaults.show_edit_predictions = Some(!show_edit_predictions)
|
||||
});
|
||||
}
|
||||
|
||||
pub fn show_worktree_trust_security_modal(
|
||||
&mut self,
|
||||
toggle: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(security_modal) = self.active_modal::<SecurityModal>(cx) {
|
||||
if toggle {
|
||||
security_modal.update(cx, |security_modal, cx| {
|
||||
security_modal.dismiss(cx);
|
||||
})
|
||||
} else {
|
||||
security_modal.update(cx, |security_modal, cx| {
|
||||
security_modal.refresh_restricted_paths(cx);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let has_restricted_worktrees = TrustedWorktrees::try_get_global(cx)
|
||||
.map(|trusted_worktrees| {
|
||||
trusted_worktrees
|
||||
.read(cx)
|
||||
.has_restricted_worktrees(&self.project().read(cx).worktree_store(), cx)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if has_restricted_worktrees {
|
||||
let project = self.project().read(cx);
|
||||
let remote_host = project.remote_connection_options(cx);
|
||||
let worktree_store = project.worktree_store().downgrade();
|
||||
self.toggle_modal(window, cx, |_, cx| {
|
||||
SecurityModal::new(worktree_store, remote_host, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn leader_border_for_pane(
|
||||
|
|
@ -7968,6 +8046,7 @@ pub fn open_remote_project_with_new_connection(
|
|||
app_state.user_store.clone(),
|
||||
app_state.languages.clone(),
|
||||
app_state.fs.clone(),
|
||||
true,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ use reqwest_client::ReqwestClient;
|
|||
use assets::Assets;
|
||||
use node_runtime::{NodeBinaryOptions, NodeRuntime};
|
||||
use parking_lot::Mutex;
|
||||
use project::project_settings::ProjectSettings;
|
||||
use project::{project_settings::ProjectSettings, trusted_worktrees};
|
||||
use recent_projects::{SshSettings, open_remote_project};
|
||||
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
||||
use session::{AppSession, Session};
|
||||
|
|
@ -36,6 +36,7 @@ use std::{
|
|||
env,
|
||||
io::{self, IsTerminal},
|
||||
path::{Path, PathBuf},
|
||||
pin::Pin,
|
||||
process,
|
||||
sync::{Arc, OnceLock},
|
||||
time::Instant,
|
||||
|
|
@ -406,6 +407,7 @@ pub fn main() {
|
|||
});
|
||||
|
||||
app.run(move |cx| {
|
||||
trusted_worktrees::init(None, None, cx);
|
||||
menu::init();
|
||||
zed_actions::init();
|
||||
|
||||
|
|
@ -474,7 +476,15 @@ pub fn main() {
|
|||
tx.send(Some(options)).log_err();
|
||||
})
|
||||
.detach();
|
||||
let node_runtime = NodeRuntime::new(client.http_client(), Some(shell_env_loaded_rx), rx);
|
||||
|
||||
let trust_task = trusted_worktrees::wait_for_default_workspace_trust("Node runtime", cx)
|
||||
.map(|trust_task| Box::pin(trust_task) as Pin<Box<_>>);
|
||||
let node_runtime = NodeRuntime::new(
|
||||
client.http_client(),
|
||||
Some(shell_env_loaded_rx),
|
||||
rx,
|
||||
trust_task,
|
||||
);
|
||||
|
||||
debug_adapter_extension::init(extension_host_proxy.clone(), cx);
|
||||
languages::init(languages.clone(), fs.clone(), node_runtime.clone(), cx);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@
|
|||
- [Visual Customization](./visual-customization.md)
|
||||
- [Vim Mode](./vim.md)
|
||||
- [Helix Mode](./helix.md)
|
||||
- [Privacy and Security](./ai/privacy-and-security.md)
|
||||
- [Worktree Trust](./worktree-trust.md)
|
||||
- [AI Improvement](./ai/ai-improvement.md)
|
||||
|
||||
<!-- - [Globs](./globs.md) -->
|
||||
<!-- - [Fonts](./fonts.md) -->
|
||||
|
|
@ -69,8 +72,6 @@
|
|||
- [Models](./ai/models.md)
|
||||
- [Plans and Usage](./ai/plans-and-usage.md)
|
||||
- [Billing](./ai/billing.md)
|
||||
- [Privacy and Security](./ai/privacy-and-security.md)
|
||||
- [AI Improvement](./ai/ai-improvement.md)
|
||||
|
||||
# Extensions
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Philosophy
|
||||
|
||||
Zed aims to collect on the minimum data necessary to serve and improve our product.
|
||||
Zed aims to collect only the minimum data necessary to serve and improve our product.
|
||||
|
||||
We believe in opt-in data sharing as the default in building AI products, rather than opt-out, like most of our competitors. Privacy Mode is not a setting to be toggled, it's a default stance.
|
||||
|
||||
|
|
@ -12,6 +12,8 @@ It is entirely possible to use Zed, including Zed's AI capabilities, without sha
|
|||
|
||||
## Documentation
|
||||
|
||||
- [Worktree trust](../worktree-trust.md): How Zed opens files and directories in restricted mode.
|
||||
|
||||
- [Telemetry](../telemetry.md): How Zed collects general telemetry data.
|
||||
|
||||
- [AI Improvement](./ai-improvement.md): Zed's opt-in-only approach to data collection for AI improvement, whether our Agentic offering or Edit Predictions.
|
||||
|
|
|
|||
|
|
@ -1451,6 +1451,47 @@ 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 worktree and workspace trust checks:
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
"session": {
|
||||
"trust_all_worktrees": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When trusted, project settings are synchronized automatically, language and MCP servers are downloaded and started automatically.
|
||||
|
||||
### 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.
|
||||
|
|
|
|||
66
docs/src/worktree-trust.md
Normal file
66
docs/src/worktree-trust.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# Zed and trusted worktrees
|
||||
|
||||
A worktree in Zed is either a directory or a single file that Zed opens as a standalone "project".
|
||||
Zed opens a worktree every time `zed some/path` is invoked, on drag and dropping a file or directory into Zed, on opening user settings.json, etc.
|
||||
|
||||
Every worktree opened may contain a `.zed/settings.json` file with extra configuration options that may require installing and spawning language servers or MCP servers.
|
||||
Note that the Zed workspace itself may also perform user-configured MCP server installation and spawning, even if no worktrees are open.
|
||||
|
||||
In order to provide users the opportunity to make their own choices according to their unique threat model and risk tolerance, the workspace and all worktrees will be started in Restricted mode, which prevents download and execution of any related items. Until configured to trust the workspace and/or worktrees, Zed will not perform any untrusted actions and will wait for user confirmation. This gives users a chance to review and understand any pre-configured settings, MCP servers, or language servers associated with a project.
|
||||
|
||||
If a worktree is not trusted, Zed will indicate this with an exclamation mark icon in the title bar and a message in the Agent panel. Clicking this icon or using `workspace::ToggleWorktreeSecurity` action will bring up the security modal that allows the user to trust the worktree.
|
||||
|
||||
Trusting any worktree will persist this information between restarts. It's possible to clear all trusted worktrees with `workspace::ClearTrustedWorktrees` command.
|
||||
This command will restart Zed, to ensure no untrusted settings, language servers or MCP servers persist.
|
||||
|
||||
This feature works locally and on SSH and WSL remote hosts. Zed tracks trust information per host in these cases.
|
||||
|
||||
## What is restricted
|
||||
|
||||
Restricted Mode prevents:
|
||||
|
||||
- Project settings (`.zed/settings.json`) from being parsed and applied
|
||||
- Language servers from being installed and spawned
|
||||
- MCP servers from being installed and spawned
|
||||
|
||||
## Configuring broad worktree trust
|
||||
|
||||
By default, Zed won't trust any new worktrees and users will be required to trust each new worktree. Though not recommended, users may elect to trust all worktrees and the current workspace for a given session by configuring the following setting:
|
||||
|
||||
```json [settings]
|
||||
"session": {
|
||||
"trust_all_worktrees": true
|
||||
}
|
||||
```
|
||||
|
||||
Note that auto trusted worktrees are not persisted between restarts, only manually trusted worktrees are. This ensures that new trust decisions must be made if a users elects to disable the `trust_all_worktrees` setting.
|
||||
|
||||
## Trust hierarchy
|
||||
|
||||
These are mostly internal details and may change in the future, but are helpful to understand how multiple different trust requests can be approved at once.
|
||||
Zed has multiple layers of trust, based on the requests, from the least to most trusted level:
|
||||
|
||||
- "single file worktree"
|
||||
|
||||
After opening an empty Zed it's possible to open just a file, same as after opening a directory in Zed it's possible to open a file outside of this directory.
|
||||
A typical scenario where a directory might be open and a single file is subsequently opened is opening Zed's settings.json file via `zed: open settings file` command: that starts a language server for a new file open, which originates from a newly created, single file worktree.
|
||||
|
||||
Spawning a language server presents a risk should the language server experience a supply-chain attack; therefore, Zed restricts that by default. Each single file worktree requires a separate trust grant, unless the directory containing it is trusted or all worktrees are trusted.
|
||||
|
||||
- "workspace"
|
||||
|
||||
Even an empty Zed workspace with no files or directories open presents a risk if new MCP servers are locally configured by the user without review. For instance, opening an Assistant Panel and creating a new external agent thread might require installing and running new user-configured [Model Context Protocol servers](./ai/mcp.md). By default, zed will restrict a new MCP server until the user elects to trust the local workspace. Users may also disable the entire Agent panel if preferred; see [AI Configuration](./ai/configuration.md) for more details.
|
||||
|
||||
Workspace trust, permitted by trusting Zed with no worktrees open, allows locally configured resources to be downloaded and executed. Workspace trust is per host and also trusts all single file worktrees from the same host in order to permit all local user-configured MCP and language servers to start.
|
||||
|
||||
- "directory worktree"
|
||||
|
||||
If a directory is open in Zed, it's a full worktree which may spawn multiple language servers associated with it or spawn MCP servers if contained in a project settings file.Therefore, each directory worktree requires a separate trust grant unless a parent directory worktree trust is granted (see below).
|
||||
|
||||
When a directory worktree is trusted, language and MCP servers are permitted to be downloaded and started, hence we also enable workspace trust for the host in question automatically when this occurs.
|
||||
|
||||
- "parent directory worktree"
|
||||
|
||||
To permit trust decisions for multiple directory worktrees at once, it's possible to trust all subdirectories of a given parent directory worktree opened in Zed by checking the appropriate checkbox. This will grant trust to all its subdirectories, including all current and potential directory worktrees.
|
||||
|
||||
This also automatically enables workspace trust to permit the newly trusted resources to download and start.
|
||||
Loading…
Reference in a new issue