Sync config with ssh remotes (#17349)

Release Notes:

- N/A

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
Conrad Irwin 2024-09-04 12:28:51 -06:00 committed by GitHub
parent 4b094798e0
commit 7fb94c4c4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 609 additions and 193 deletions

View file

@ -11,39 +11,49 @@ use gpui::{AppContext, Context, Model, ModelContext, Task};
use settings::Settings as _;
use worktree::WorktreeId;
use crate::project_settings::{DirenvSettings, ProjectSettings};
use crate::{
project_settings::{DirenvSettings, ProjectSettings},
worktree_store::{WorktreeStore, WorktreeStoreEvent},
};
pub(crate) struct ProjectEnvironment {
pub struct ProjectEnvironment {
cli_environment: Option<HashMap<String, String>>,
get_environment_task: Option<Shared<Task<Option<HashMap<String, String>>>>>,
cached_shell_environments: HashMap<WorktreeId, HashMap<String, String>>,
}
impl ProjectEnvironment {
pub(crate) fn new(
pub fn new(
worktree_store: &Model<WorktreeStore>,
cli_environment: Option<HashMap<String, String>>,
cx: &mut AppContext,
) -> Model<Self> {
cx.new_model(|_| Self {
cli_environment,
get_environment_task: None,
cached_shell_environments: Default::default(),
cx.new_model(|cx| {
cx.subscribe(worktree_store, |this: &mut Self, _, event, _| match event {
WorktreeStoreEvent::WorktreeRemoved(_, id) => {
this.remove_worktree_environment(*id);
}
_ => {}
})
.detach();
Self {
cli_environment,
get_environment_task: None,
cached_shell_environments: Default::default(),
}
})
}
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn test(
pub(crate) fn set_cached(
&mut self,
shell_environments: &[(WorktreeId, HashMap<String, String>)],
cx: &mut AppContext,
) -> Model<Self> {
cx.new_model(|_| Self {
cli_environment: None,
get_environment_task: None,
cached_shell_environments: shell_environments
.iter()
.cloned()
.collect::<HashMap<_, _>>(),
})
) {
self.cached_shell_environments = shell_environments
.iter()
.cloned()
.collect::<HashMap<_, _>>();
}
pub(crate) fn remove_worktree_environment(&mut self, worktree_id: WorktreeId) {

View file

@ -5,7 +5,7 @@ use crate::{
lsp_ext_command,
project_settings::ProjectSettings,
relativize_path, resolve_path,
worktree_store::WorktreeStore,
worktree_store::{WorktreeStore, WorktreeStoreEvent},
yarn::YarnPathStore,
CodeAction, Completion, CoreCompletion, Hover, InlayHint, Item as _, ProjectPath,
ProjectTransaction, ResolveState, Symbol,
@ -89,7 +89,7 @@ pub struct LspStore {
downstream_client: Option<AnyProtoClient>,
upstream_client: Option<AnyProtoClient>,
project_id: u64,
http_client: Arc<dyn HttpClient>,
http_client: Option<Arc<dyn HttpClient>>,
fs: Arc<dyn Fs>,
nonce: u128,
buffer_store: Model<BufferStore>,
@ -210,12 +210,12 @@ impl LspStore {
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
pub fn new(
buffer_store: Model<BufferStore>,
worktree_store: Model<WorktreeStore>,
environment: Option<Model<ProjectEnvironment>>,
languages: Arc<LanguageRegistry>,
http_client: Arc<dyn HttpClient>,
http_client: Option<Arc<dyn HttpClient>>,
fs: Arc<dyn Fs>,
downstream_client: Option<AnyProtoClient>,
upstream_client: Option<AnyProtoClient>,
@ -225,6 +225,8 @@ impl LspStore {
let yarn = YarnPathStore::new(fs.clone(), cx);
cx.subscribe(&buffer_store, Self::on_buffer_store_event)
.detach();
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
.detach();
Self {
downstream_client,
@ -278,6 +280,31 @@ impl LspStore {
}
}
fn on_worktree_store_event(
&mut self,
_: Model<WorktreeStore>,
event: &WorktreeStoreEvent,
cx: &mut ModelContext<Self>,
) {
match event {
WorktreeStoreEvent::WorktreeAdded(worktree) => {
if !worktree.read(cx).is_local() {
return;
}
cx.subscribe(worktree, |this, worktree, event, cx| match event {
worktree::Event::UpdatedEntries(changes) => {
this.update_local_worktree_language_servers(&worktree, changes, cx);
}
worktree::Event::UpdatedGitRepositories(_)
| worktree::Event::DeletedEntry(_) => {}
})
.detach()
}
WorktreeStoreEvent::WorktreeRemoved(_, id) => self.remove_worktree(*id, cx),
WorktreeStoreEvent::WorktreeOrderChanged => {}
}
}
fn on_buffer_event(
&mut self,
buffer: Model<Buffer>,
@ -463,11 +490,6 @@ impl LspStore {
self.buffer_store.clone()
}
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn set_environment(&mut self, environment: Model<ProjectEnvironment>) {
self.environment = Some(environment);
}
pub fn set_active_entry(&mut self, active_entry: Option<ProjectEntryId>) {
self.active_entry = active_entry;
}
@ -6105,11 +6127,15 @@ impl ProjectLspAdapterDelegate {
Task::ready(None).shared()
};
let Some(http_client) = lsp_store.http_client.clone() else {
panic!("ProjectLspAdapterDelegate cannot be constructedd on an ssh-remote yet")
};
Arc::new(Self {
lsp_store: cx.weak_model(),
worktree: worktree.read(cx).snapshot(),
fs: lsp_store.fs.clone(),
http_client: lsp_store.http_client.clone(),
http_client,
language_registry: lsp_store.languages.clone(),
load_shell_env_task,
})

View file

@ -27,7 +27,7 @@ use client::{
use clock::ReplicaId;
use collections::{BTreeSet, HashMap, HashSet};
use debounced_delay::DebouncedDelay;
use environment::ProjectEnvironment;
pub use environment::ProjectEnvironment;
use futures::{
channel::mpsc::{self, UnboundedReceiver},
future::try_join_all,
@ -58,12 +58,9 @@ use lsp::{CompletionContext, DocumentHighlightKind, LanguageServer, LanguageServ
use lsp_command::*;
use node_runtime::NodeRuntime;
use parking_lot::{Mutex, RwLock};
use paths::{
local_settings_file_relative_path, local_tasks_file_relative_path,
local_vscode_tasks_file_relative_path,
};
use paths::{local_tasks_file_relative_path, local_vscode_tasks_file_relative_path};
use prettier_support::{DefaultPrettier, PrettierInstance};
use project_settings::{LspSettings, ProjectSettings};
use project_settings::{LspSettings, ProjectSettings, SettingsObserver};
use remote::SshSession;
use rpc::{
proto::{AnyProtoClient, SSH_PROJECT_ID},
@ -174,6 +171,7 @@ pub struct Project {
last_formatting_failure: Option<String>,
buffers_being_formatted: HashSet<BufferId>,
environment: Model<ProjectEnvironment>,
settings_observer: Model<SettingsObserver>,
}
#[derive(Default)]
@ -505,6 +503,14 @@ impl FormatTrigger {
}
}
enum EntitySubscription {
Project(PendingEntitySubscription<Project>),
BufferStore(PendingEntitySubscription<BufferStore>),
WorktreeStore(PendingEntitySubscription<WorktreeStore>),
LspStore(PendingEntitySubscription<LspStore>),
SettingsObserver(PendingEntitySubscription<SettingsObserver>),
}
#[derive(Clone)]
pub enum DirectoryLister {
Project(Model<Project>),
@ -584,7 +590,6 @@ impl Project {
client.add_model_message_handler(Self::handle_unshare_project);
client.add_model_request_handler(Self::handle_update_buffer);
client.add_model_message_handler(Self::handle_update_worktree);
client.add_model_message_handler(Self::handle_update_worktree_settings);
client.add_model_request_handler(Self::handle_reload_buffers);
client.add_model_request_handler(Self::handle_synchronize_buffers);
client.add_model_request_handler(Self::handle_format_buffers);
@ -600,6 +605,7 @@ impl Project {
WorktreeStore::init(&client);
BufferStore::init(&client);
LspStore::init(&client);
SettingsObserver::init(&client);
}
pub fn local(
@ -629,14 +635,18 @@ impl Project {
cx.subscribe(&buffer_store, Self::on_buffer_store_event)
.detach();
let environment = ProjectEnvironment::new(env, cx);
let settings_observer = cx.new_model(|cx| {
SettingsObserver::new_local(fs.clone(), worktree_store.clone(), cx)
});
let environment = ProjectEnvironment::new(&worktree_store, env, cx);
let lsp_store = cx.new_model(|cx| {
LspStore::new(
buffer_store.clone(),
worktree_store.clone(),
Some(environment.clone()),
languages.clone(),
client.http_client(),
Some(client.http_client()),
fs.clone(),
None,
None,
@ -665,6 +675,7 @@ impl Project {
languages,
client,
user_store,
settings_observer,
fs,
ssh_session: None,
buffers_needing_diff: Default::default(),
@ -704,14 +715,21 @@ impl Project {
this.worktree_store.update(cx, |store, _cx| {
store.set_upstream_client(client.clone());
});
this.settings_observer = cx.new_model(|cx| {
SettingsObserver::new_ssh(ssh.clone().into(), this.worktree_store.clone(), cx)
});
ssh.subscribe_to_entity(SSH_PROJECT_ID, &cx.handle());
ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.buffer_store);
ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.worktree_store);
ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.lsp_store);
ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.settings_observer);
client.add_model_message_handler(Self::handle_update_worktree);
client.add_model_message_handler(Self::handle_create_buffer_for_peer);
client.add_model_message_handler(BufferStore::handle_update_buffer_file);
client.add_model_message_handler(BufferStore::handle_update_diff_base);
LspStore::init(&client);
SettingsObserver::init(&client);
this.ssh_session = Some(ssh);
});
@ -746,12 +764,17 @@ impl Project {
) -> Result<Model<Self>> {
client.authenticate_and_connect(true, &cx).await?;
let subscriptions = (
client.subscribe_to_entity::<Self>(remote_id)?,
client.subscribe_to_entity::<BufferStore>(remote_id)?,
client.subscribe_to_entity::<WorktreeStore>(remote_id)?,
client.subscribe_to_entity::<LspStore>(remote_id)?,
);
let subscriptions = [
EntitySubscription::Project(client.subscribe_to_entity::<Self>(remote_id)?),
EntitySubscription::BufferStore(client.subscribe_to_entity::<BufferStore>(remote_id)?),
EntitySubscription::WorktreeStore(
client.subscribe_to_entity::<WorktreeStore>(remote_id)?,
),
EntitySubscription::LspStore(client.subscribe_to_entity::<LspStore>(remote_id)?),
EntitySubscription::SettingsObserver(
client.subscribe_to_entity::<SettingsObserver>(remote_id)?,
),
];
let response = client
.request_envelope(proto::JoinProject {
project_id: remote_id,
@ -771,12 +794,7 @@ impl Project {
async fn from_join_project_response(
response: TypedEnvelope<proto::JoinProjectResponse>,
subscription: (
PendingEntitySubscription<Project>,
PendingEntitySubscription<BufferStore>,
PendingEntitySubscription<WorktreeStore>,
PendingEntitySubscription<LspStore>,
),
subscriptions: [EntitySubscription; 5],
client: Arc<Client>,
user_store: Model<UserStore>,
languages: Arc<LanguageRegistry>,
@ -803,7 +821,7 @@ impl Project {
worktree_store.clone(),
None,
languages.clone(),
client.http_client(),
Some(client.http_client()),
fs.clone(),
None,
Some(client.clone().into()),
@ -814,6 +832,9 @@ impl Project {
lsp_store
})?;
let settings_observer =
cx.new_model(|cx| SettingsObserver::new_remote(worktree_store.clone(), cx))?;
let this = cx.new_model(|cx| {
let replica_id = response.payload.replica_id as ReplicaId;
let tasks = Inventory::new(cx);
@ -850,6 +871,7 @@ impl Project {
snippets,
fs,
ssh_session: None,
settings_observer: settings_observer.clone(),
client_subscriptions: Default::default(),
_subscriptions: vec![cx.on_release(Self::release)],
client: client.clone(),
@ -876,7 +898,7 @@ impl Project {
.dev_server_project_id
.map(|dev_server_project_id| DevServerProjectId(dev_server_project_id)),
search_history: Self::new_search_history(),
environment: ProjectEnvironment::new(None, cx),
environment: ProjectEnvironment::new(&worktree_store, None, cx),
remotely_created_buffers: Arc::new(Mutex::new(RemotelyCreatedBuffers::default())),
last_formatting_failure: None,
buffers_being_formatted: Default::default(),
@ -888,12 +910,24 @@ impl Project {
this
})?;
let subscriptions = [
subscription.0.set_model(&this, &mut cx),
subscription.1.set_model(&buffer_store, &mut cx),
subscription.2.set_model(&worktree_store, &mut cx),
subscription.3.set_model(&lsp_store, &mut cx),
];
let subscriptions = subscriptions
.into_iter()
.map(|s| match s {
EntitySubscription::BufferStore(subscription) => {
subscription.set_model(&buffer_store, &mut cx)
}
EntitySubscription::WorktreeStore(subscription) => {
subscription.set_model(&worktree_store, &mut cx)
}
EntitySubscription::SettingsObserver(subscription) => {
subscription.set_model(&settings_observer, &mut cx)
}
EntitySubscription::Project(subscription) => subscription.set_model(&this, &mut cx),
EntitySubscription::LspStore(subscription) => {
subscription.set_model(&lsp_store, &mut cx)
}
})
.collect::<Vec<_>>();
let user_ids = response
.payload
@ -924,12 +958,19 @@ impl Project {
) -> Result<Model<Self>> {
client.authenticate_and_connect(true, &cx).await?;
let subscriptions = (
client.subscribe_to_entity::<Self>(remote_id.0)?,
client.subscribe_to_entity::<BufferStore>(remote_id.0)?,
client.subscribe_to_entity::<WorktreeStore>(remote_id.0)?,
client.subscribe_to_entity::<LspStore>(remote_id.0)?,
);
let subscriptions = [
EntitySubscription::Project(client.subscribe_to_entity::<Self>(remote_id.0)?),
EntitySubscription::BufferStore(
client.subscribe_to_entity::<BufferStore>(remote_id.0)?,
),
EntitySubscription::WorktreeStore(
client.subscribe_to_entity::<WorktreeStore>(remote_id.0)?,
),
EntitySubscription::LspStore(client.subscribe_to_entity::<LspStore>(remote_id.0)?),
EntitySubscription::SettingsObserver(
client.subscribe_to_entity::<SettingsObserver>(remote_id.0)?,
),
];
let response = client
.request_envelope(proto::JoinHostedProject {
project_id: remote_id.0,
@ -1047,13 +1088,10 @@ impl Project {
.unwrap();
project.update(cx, |project, cx| {
// In tests we always populate the environment to be empty so we don't run the shell
let tree_id = tree.read(cx).id();
let environment = ProjectEnvironment::test(&[(tree_id, HashMap::default())], cx);
project.environment = environment.clone();
project
.lsp_store
.update(cx, |lsp_store, _| lsp_store.set_environment(environment));
project.environment.update(cx, |environment, _| {
environment.set_cached(&[(tree_id, HashMap::default())])
});
});
tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
@ -1066,6 +1104,10 @@ impl Project {
self.lsp_store.clone()
}
pub fn worktree_store(&self) -> Model<WorktreeStore> {
self.worktree_store.clone()
}
fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) {
let mut language_servers_to_start = Vec::new();
let mut language_formatters_to_check = Vec::new();
@ -1499,6 +1541,9 @@ impl Project {
self.client
.subscribe_to_entity(project_id)?
.set_model(&self.lsp_store, &mut cx.to_async()),
self.client
.subscribe_to_entity(project_id)?
.set_model(&self.settings_observer, &mut cx.to_async()),
]);
self.buffer_store.update(cx, |buffer_store, cx| {
@ -1510,21 +1555,9 @@ impl Project {
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.shared(project_id, self.client.clone().into(), cx)
});
let store = cx.global::<SettingsStore>();
for worktree in self.worktrees(cx) {
let worktree_id = worktree.read(cx).id().to_proto();
for (path, content) in store.local_settings(worktree.entity_id().as_u64() as usize) {
self.client
.send(proto::UpdateWorktreeSettings {
project_id,
worktree_id,
path: path.to_string_lossy().into(),
content: Some(content),
})
.log_err();
}
}
self.settings_observer.update(cx, |settings_observer, cx| {
settings_observer.shared(project_id, self.client.clone().into(), cx)
});
self.client_state = ProjectClientState::Shared {
remote_id: project_id,
@ -1608,6 +1641,9 @@ impl Project {
buffer_store.forget_shared_buffers();
buffer_store.unshared(cx)
});
self.settings_observer.update(cx, |settings_observer, cx| {
settings_observer.unshared(cx);
});
self.client
.send(proto::UnshareProject {
project_id: remote_id,
@ -2147,10 +2183,6 @@ impl Project {
match event {
worktree::Event::UpdatedEntries(changes) => {
if is_local {
this.lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_local_worktree_language_servers(&worktree, changes, cx);
});
this.update_local_worktree_settings(&worktree, changes, cx);
this.update_prettier_settings(&worktree, changes, cx);
}
@ -2198,12 +2230,6 @@ impl Project {
}
return;
}
self.environment.update(cx, |environment, _| {
environment.remove_worktree_environment(id_to_remove);
});
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.remove_worktree(id_to_remove, cx);
});
let mut prettier_instances_to_clean = FuturesUnordered::new();
if let Some(prettier_paths) = self.prettiers_per_worktree.remove(&id_to_remove) {
@ -3818,11 +3844,8 @@ impl Project {
if worktree.read(cx).is_remote() {
return;
}
let project_id = self.remote_id();
let worktree_id = worktree.entity_id();
let remote_worktree_id = worktree.read(cx).id();
let mut settings_contents = Vec::new();
for (path, _, change) in changes.iter() {
let removed = change == &PathChange::Removed;
let abs_path = match worktree.read(cx).absolutize(path) {
@ -3833,24 +3856,7 @@ impl Project {
}
};
if path.ends_with(local_settings_file_relative_path()) {
let settings_dir = Arc::from(
path.ancestors()
.nth(local_settings_file_relative_path().components().count())
.unwrap(),
);
let fs = self.fs.clone();
settings_contents.push(async move {
(
settings_dir,
if removed {
None
} else {
Some(async move { fs.load(&abs_path).await }.await)
},
)
});
} else if path.ends_with(local_tasks_file_relative_path()) {
if path.ends_with(local_tasks_file_relative_path()) {
self.task_inventory().update(cx, |task_inventory, cx| {
if removed {
task_inventory.remove_local_static_source(&abs_path);
@ -3898,43 +3904,6 @@ impl Project {
})
}
}
if settings_contents.is_empty() {
return;
}
let client = self.client.clone();
cx.spawn(move |_, cx| async move {
let settings_contents: Vec<(Arc<Path>, _)> =
futures::future::join_all(settings_contents).await;
cx.update(|cx| {
cx.update_global::<SettingsStore, _>(|store, cx| {
for (directory, file_content) in settings_contents {
let file_content = file_content.and_then(|content| content.log_err());
store
.set_local_settings(
worktree_id.as_u64() as usize,
directory.clone(),
file_content.as_deref(),
cx,
)
.log_err();
if let Some(remote_id) = project_id {
client
.send(proto::UpdateWorktreeSettings {
project_id: remote_id,
worktree_id: remote_worktree_id.to_proto(),
path: directory.to_string_lossy().into_owned(),
content: file_content,
})
.log_err();
}
}
});
})
.ok();
})
.detach();
}
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
@ -4236,29 +4205,6 @@ impl Project {
})?
}
async fn handle_update_worktree_settings(
this: Model<Self>,
envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
if let Some(worktree) = this.worktree_for_id(worktree_id, cx) {
cx.update_global::<SettingsStore, _>(|store, cx| {
store
.set_local_settings(
worktree.entity_id().as_u64() as usize,
PathBuf::from(&envelope.payload.path).into(),
envelope.payload.content.as_deref(),
cx,
)
.log_err();
});
}
Ok(())
})?
}
async fn handle_update_buffer(
this: Model<Self>,
envelope: TypedEnvelope<proto::UpdateBuffer>,

View file

@ -1,9 +1,23 @@
use collections::HashMap;
use gpui::AppContext;
use fs::Fs;
use gpui::{AppContext, AsyncAppContext, BorrowAppContext, Model, ModelContext};
use paths::local_settings_file_relative_path;
use rpc::{
proto::{self, AnyProtoClient},
TypedEnvelope,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use std::{sync::Arc, time::Duration};
use settings::{Settings, SettingsSources, SettingsStore};
use std::{
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use util::ResultExt;
use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct ProjectSettings {
@ -157,3 +171,276 @@ impl Settings for ProjectSettings {
sources.json_merge()
}
}
pub enum SettingsObserverMode {
Local(Arc<dyn Fs>),
Ssh(AnyProtoClient),
Remote,
}
pub struct SettingsObserver {
mode: SettingsObserverMode,
downstream_client: Option<AnyProtoClient>,
worktree_store: Model<WorktreeStore>,
project_id: u64,
}
/// SettingsObserver observers changes to .zed/settings.json files in local worktrees
/// (or the equivalent protobuf messages from upstream) and updates local settings
/// and sends notifications downstream.
/// In ssh mode it also monitors ~/.config/zed/settings.json and sends the content
/// upstream.
impl SettingsObserver {
pub fn init(client: &AnyProtoClient) {
client.add_model_message_handler(Self::handle_update_worktree_settings);
client.add_model_message_handler(Self::handle_update_user_settings)
}
pub fn new_local(
fs: Arc<dyn Fs>,
worktree_store: Model<WorktreeStore>,
cx: &mut ModelContext<Self>,
) -> Self {
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
.detach();
Self {
worktree_store,
mode: SettingsObserverMode::Local(fs),
downstream_client: None,
project_id: 0,
}
}
pub fn new_ssh(
client: AnyProtoClient,
worktree_store: Model<WorktreeStore>,
cx: &mut ModelContext<Self>,
) -> Self {
let this = Self {
worktree_store,
mode: SettingsObserverMode::Ssh(client.clone()),
downstream_client: None,
project_id: 0,
};
this.maintain_ssh_settings(client, cx);
this
}
pub fn new_remote(worktree_store: Model<WorktreeStore>, _: &mut ModelContext<Self>) -> Self {
Self {
worktree_store,
mode: SettingsObserverMode::Remote,
downstream_client: None,
project_id: 0,
}
}
pub fn shared(
&mut self,
project_id: u64,
downstream_client: AnyProtoClient,
cx: &mut ModelContext<Self>,
) {
self.project_id = project_id;
self.downstream_client = Some(downstream_client.clone());
let store = cx.global::<SettingsStore>();
for worktree in self.worktree_store.read(cx).worktrees() {
let worktree_id = worktree.read(cx).id().to_proto();
for (path, content) in store.local_settings(worktree.entity_id().as_u64() as usize) {
downstream_client
.send(proto::UpdateWorktreeSettings {
project_id,
worktree_id,
path: path.to_string_lossy().into(),
content: Some(content),
})
.log_err();
}
}
}
pub fn unshared(&mut self, _: &mut ModelContext<Self>) {
self.downstream_client = None;
}
async fn handle_update_worktree_settings(
this: Model<Self>,
envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
mut cx: AsyncAppContext,
) -> anyhow::Result<()> {
this.update(&mut cx, |this, cx| {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let Some(worktree) = this
.worktree_store
.read(cx)
.worktree_for_id(worktree_id, cx)
else {
return;
};
this.update_settings(
worktree,
[(
PathBuf::from(&envelope.payload.path).into(),
envelope.payload.content,
)],
cx,
);
})?;
Ok(())
}
pub async fn handle_update_user_settings(
_: Model<Self>,
envelope: TypedEnvelope<proto::UpdateUserSettings>,
mut cx: AsyncAppContext,
) -> anyhow::Result<()> {
cx.update_global(move |settings_store: &mut SettingsStore, cx| {
settings_store.set_user_settings(&envelope.payload.content, cx)
})??;
Ok(())
}
pub fn maintain_ssh_settings(&self, ssh: AnyProtoClient, cx: &mut ModelContext<Self>) {
let mut settings = cx.global::<SettingsStore>().raw_user_settings().clone();
if let Some(content) = serde_json::to_string(&settings).log_err() {
ssh.send(proto::UpdateUserSettings {
project_id: 0,
content,
})
.log_err();
}
cx.observe_global::<SettingsStore>(move |_, cx| {
let new_settings = cx.global::<SettingsStore>().raw_user_settings();
if &settings != new_settings {
settings = new_settings.clone()
}
if let Some(content) = serde_json::to_string(&settings).log_err() {
ssh.send(proto::UpdateUserSettings {
project_id: 0,
content,
})
.log_err();
}
})
.detach();
}
fn on_worktree_store_event(
&mut self,
_: Model<WorktreeStore>,
event: &WorktreeStoreEvent,
cx: &mut ModelContext<Self>,
) {
match event {
WorktreeStoreEvent::WorktreeAdded(worktree) => cx
.subscribe(worktree, |this, worktree, event, cx| match event {
worktree::Event::UpdatedEntries(changes) => {
this.update_local_worktree_settings(&worktree, changes, cx)
}
_ => {}
})
.detach(),
_ => {}
}
}
fn update_local_worktree_settings(
&mut self,
worktree: &Model<Worktree>,
changes: &UpdatedEntriesSet,
cx: &mut ModelContext<Self>,
) {
let SettingsObserverMode::Local(fs) = &self.mode else {
return;
};
let mut settings_contents = Vec::new();
for (path, _, change) in changes.iter() {
let removed = change == &PathChange::Removed;
let abs_path = match worktree.read(cx).absolutize(path) {
Ok(abs_path) => abs_path,
Err(e) => {
log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
continue;
}
};
if path.ends_with(local_settings_file_relative_path()) {
let settings_dir = Arc::from(
path.ancestors()
.nth(local_settings_file_relative_path().components().count())
.unwrap(),
);
let fs = fs.clone();
settings_contents.push(async move {
(
settings_dir,
if removed {
None
} else {
Some(async move { fs.load(&abs_path).await }.await)
},
)
});
}
}
if settings_contents.is_empty() {
return;
}
let worktree = worktree.clone();
cx.spawn(move |this, cx| async move {
let settings_contents: Vec<(Arc<Path>, _)> =
futures::future::join_all(settings_contents).await;
cx.update(|cx| {
this.update(cx, |this, cx| {
this.update_settings(
worktree,
settings_contents
.into_iter()
.map(|(path, content)| (path, content.and_then(|c| c.log_err()))),
cx,
)
})
})
})
.detach();
}
fn update_settings(
&mut self,
worktree: Model<Worktree>,
settings_contents: impl IntoIterator<Item = (Arc<Path>, Option<String>)>,
cx: &mut ModelContext<Self>,
) {
let worktree_id = worktree.entity_id();
let remote_worktree_id = worktree.read(cx).id();
cx.update_global::<SettingsStore, _>(|store, cx| {
for (directory, file_content) in settings_contents {
store
.set_local_settings(
worktree_id.as_u64() as usize,
directory.clone(),
file_content.as_deref(),
cx,
)
.log_err();
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_string_lossy().into_owned(),
content: file_content,
})
.log_err();
}
}
})
}
}