1use anyhow::Context as _;
2use collections::HashMap;
3use dap::adapters::DebugAdapterName;
4use fs::Fs;
5use futures::StreamExt as _;
6use gpui::{App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Task};
7use lsp::LanguageServerName;
8use paths::{
9 EDITORCONFIG_NAME, local_debug_file_relative_path, local_settings_file_relative_path,
10 local_tasks_file_relative_path, local_vscode_launch_file_relative_path,
11 local_vscode_tasks_file_relative_path,
12};
13use rpc::{
14 AnyProtoClient, TypedEnvelope,
15 proto::{self, FromProto, ToProto},
16};
17use schemars::JsonSchema;
18use serde::{Deserialize, Serialize};
19use settings::{
20 InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources,
21 SettingsStore, TaskKind, parse_json_with_comments, watch_config_file,
22};
23use std::{
24 path::{Path, PathBuf},
25 sync::Arc,
26 time::Duration,
27};
28use task::{DebugTaskFile, TaskTemplates, VsCodeDebugTaskFile, VsCodeTaskFile};
29use util::{ResultExt, serde::default_true};
30use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
31
32use crate::{
33 task_store::{TaskSettingsLocation, TaskStore},
34 worktree_store::{WorktreeStore, WorktreeStoreEvent},
35};
36
37#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
38pub struct ProjectSettings {
39 /// Configuration for language servers.
40 ///
41 /// The following settings can be overridden for specific language servers:
42 /// - initialization_options
43 ///
44 /// To override settings for a language, add an entry for that language server's
45 /// name to the lsp value.
46 /// Default: null
47 #[serde(default)]
48 pub lsp: HashMap<LanguageServerName, LspSettings>,
49
50 /// Configuration for Debugger-related features
51 #[serde(default)]
52 pub dap: HashMap<DebugAdapterName, DapSettings>,
53
54 /// Configuration for Diagnostics-related features.
55 #[serde(default)]
56 pub diagnostics: DiagnosticsSettings,
57
58 /// Configuration for Git-related features
59 #[serde(default)]
60 pub git: GitSettings,
61
62 /// Configuration for Node-related features
63 #[serde(default)]
64 pub node: NodeBinarySettings,
65
66 /// Configuration for how direnv configuration should be loaded
67 #[serde(default)]
68 pub load_direnv: DirenvSettings,
69
70 /// Configuration for session-related features
71 #[serde(default)]
72 pub session: SessionSettings,
73}
74
75#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
76#[serde(rename_all = "snake_case")]
77pub struct DapSettings {
78 pub binary: Option<String>,
79}
80
81#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
82pub struct NodeBinarySettings {
83 /// The path to the Node binary.
84 pub path: Option<String>,
85 /// The path to the npm binary Zed should use (defaults to `.path/../npm`).
86 pub npm_path: Option<String>,
87 /// If disabled, Zed will download its own copy of Node.
88 #[serde(default)]
89 pub ignore_system_version: Option<bool>,
90}
91
92#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
93#[serde(rename_all = "snake_case")]
94pub enum DirenvSettings {
95 /// Load direnv configuration through a shell hook
96 ShellHook,
97 /// Load direnv configuration directly using `direnv export json`
98 #[default]
99 Direct,
100}
101
102#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
103pub struct DiagnosticsSettings {
104 /// Whether or not to include warning diagnostics
105 #[serde(default = "true_value")]
106 pub include_warnings: bool,
107
108 /// Settings for showing inline diagnostics
109 #[serde(default)]
110 pub inline: InlineDiagnosticsSettings,
111}
112
113#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
114pub struct InlineDiagnosticsSettings {
115 /// Whether or not to show inline diagnostics
116 ///
117 /// Default: false
118 #[serde(default)]
119 pub enabled: bool,
120 /// Whether to only show the inline diaganostics after a delay after the
121 /// last editor event.
122 ///
123 /// Default: 150
124 #[serde(default = "default_inline_diagnostics_debounce_ms")]
125 pub update_debounce_ms: u64,
126 /// The amount of padding between the end of the source line and the start
127 /// of the inline diagnostic in units of columns.
128 ///
129 /// Default: 4
130 #[serde(default = "default_inline_diagnostics_padding")]
131 pub padding: u32,
132 /// The minimum column to display inline diagnostics. This setting can be
133 /// used to horizontally align inline diagnostics at some position. Lines
134 /// longer than this value will still push diagnostics further to the right.
135 ///
136 /// Default: 0
137 #[serde(default)]
138 pub min_column: u32,
139
140 #[serde(default)]
141 pub max_severity: Option<DiagnosticSeverity>,
142}
143
144#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
145#[serde(rename_all = "snake_case")]
146pub enum DiagnosticSeverity {
147 Error,
148 Warning,
149 Info,
150 Hint,
151}
152
153impl Default for InlineDiagnosticsSettings {
154 fn default() -> Self {
155 Self {
156 enabled: false,
157 update_debounce_ms: default_inline_diagnostics_debounce_ms(),
158 padding: default_inline_diagnostics_padding(),
159 min_column: 0,
160 max_severity: None,
161 }
162 }
163}
164
165fn default_inline_diagnostics_debounce_ms() -> u64 {
166 150
167}
168
169fn default_inline_diagnostics_padding() -> u32 {
170 4
171}
172
173#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
174pub struct GitSettings {
175 /// Whether or not to show the git gutter.
176 ///
177 /// Default: tracked_files
178 pub git_gutter: Option<GitGutterSetting>,
179 /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
180 ///
181 /// Default: null
182 pub gutter_debounce: Option<u64>,
183 /// Whether or not to show git blame data inline in
184 /// the currently focused line.
185 ///
186 /// Default: on
187 pub inline_blame: Option<InlineBlameSettings>,
188 /// How hunks are displayed visually in the editor.
189 ///
190 /// Default: staged_hollow
191 pub hunk_style: Option<GitHunkStyleSetting>,
192}
193
194impl GitSettings {
195 pub fn inline_blame_enabled(&self) -> bool {
196 #[allow(unknown_lints, clippy::manual_unwrap_or_default)]
197 match self.inline_blame {
198 Some(InlineBlameSettings { enabled, .. }) => enabled,
199 _ => false,
200 }
201 }
202
203 pub fn inline_blame_delay(&self) -> Option<Duration> {
204 match self.inline_blame {
205 Some(InlineBlameSettings {
206 delay_ms: Some(delay_ms),
207 ..
208 }) if delay_ms > 0 => Some(Duration::from_millis(delay_ms)),
209 _ => None,
210 }
211 }
212
213 pub fn show_inline_commit_summary(&self) -> bool {
214 match self.inline_blame {
215 Some(InlineBlameSettings {
216 show_commit_summary,
217 ..
218 }) => show_commit_summary,
219 _ => false,
220 }
221 }
222}
223
224#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
225#[serde(rename_all = "snake_case")]
226pub enum GitHunkStyleSetting {
227 /// Show unstaged hunks with a filled background and staged hunks hollow.
228 #[default]
229 StagedHollow,
230 /// Show unstaged hunks hollow and staged hunks with a filled background.
231 UnstagedHollow,
232}
233
234#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
235#[serde(rename_all = "snake_case")]
236pub enum GitGutterSetting {
237 /// Show git gutter in tracked files.
238 #[default]
239 TrackedFiles,
240 /// Hide git gutter
241 Hide,
242}
243
244#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
245#[serde(rename_all = "snake_case")]
246pub struct InlineBlameSettings {
247 /// Whether or not to show git blame data inline in
248 /// the currently focused line.
249 ///
250 /// Default: true
251 #[serde(default = "true_value")]
252 pub enabled: bool,
253 /// Whether to only show the inline blame information
254 /// after a delay once the cursor stops moving.
255 ///
256 /// Default: 0
257 pub delay_ms: Option<u64>,
258 /// The minimum column number to show the inline blame information at
259 ///
260 /// Default: 0
261 pub min_column: Option<u32>,
262 /// Whether to show commit summary as part of the inline blame.
263 ///
264 /// Default: false
265 #[serde(default)]
266 pub show_commit_summary: bool,
267}
268
269const fn true_value() -> bool {
270 true
271}
272
273#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
274pub struct BinarySettings {
275 pub path: Option<String>,
276 pub arguments: Option<Vec<String>>,
277 // this can't be an FxHashMap because the extension APIs require the default SipHash
278 pub env: Option<std::collections::HashMap<String, String, std::hash::RandomState>>,
279 pub ignore_system_version: Option<bool>,
280}
281
282#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
283#[serde(rename_all = "snake_case")]
284pub struct LspSettings {
285 pub binary: Option<BinarySettings>,
286 pub initialization_options: Option<serde_json::Value>,
287 pub settings: Option<serde_json::Value>,
288 /// If the server supports sending tasks over LSP extensions,
289 /// this setting can be used to enable or disable them in Zed.
290 /// Default: true
291 #[serde(default = "default_true")]
292 pub enable_lsp_tasks: bool,
293}
294
295impl Default for LspSettings {
296 fn default() -> Self {
297 Self {
298 binary: None,
299 initialization_options: None,
300 settings: None,
301 enable_lsp_tasks: true,
302 }
303 }
304}
305
306#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
307pub struct SessionSettings {
308 /// Whether or not to restore unsaved buffers on restart.
309 ///
310 /// If this is true, user won't be prompted whether to save/discard
311 /// dirty files when closing the application.
312 ///
313 /// Default: true
314 pub restore_unsaved_buffers: bool,
315}
316
317impl Default for SessionSettings {
318 fn default() -> Self {
319 Self {
320 restore_unsaved_buffers: true,
321 }
322 }
323}
324
325impl Settings for ProjectSettings {
326 const KEY: Option<&'static str> = None;
327
328 type FileContent = Self;
329
330 fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
331 sources.json_merge()
332 }
333}
334
335pub enum SettingsObserverMode {
336 Local(Arc<dyn Fs>),
337 Remote,
338}
339
340#[derive(Clone, Debug, PartialEq)]
341pub enum SettingsObserverEvent {
342 LocalSettingsUpdated(Result<PathBuf, InvalidSettingsError>),
343 LocalTasksUpdated(Result<PathBuf, InvalidSettingsError>),
344}
345
346impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
347
348pub struct SettingsObserver {
349 mode: SettingsObserverMode,
350 downstream_client: Option<AnyProtoClient>,
351 worktree_store: Entity<WorktreeStore>,
352 project_id: u64,
353 task_store: Entity<TaskStore>,
354 _global_task_config_watchers: (Task<()>, Task<()>),
355}
356
357/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
358/// (or the equivalent protobuf messages from upstream) and updates local settings
359/// and sends notifications downstream.
360/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
361/// upstream.
362impl SettingsObserver {
363 pub fn init(client: &AnyProtoClient) {
364 client.add_entity_message_handler(Self::handle_update_worktree_settings);
365 }
366
367 pub fn new_local(
368 fs: Arc<dyn Fs>,
369 worktree_store: Entity<WorktreeStore>,
370 task_store: Entity<TaskStore>,
371 cx: &mut Context<Self>,
372 ) -> Self {
373 cx.subscribe(&worktree_store, Self::on_worktree_store_event)
374 .detach();
375
376 Self {
377 worktree_store,
378 task_store,
379 mode: SettingsObserverMode::Local(fs.clone()),
380 downstream_client: None,
381 project_id: 0,
382 _global_task_config_watchers: (
383 Self::subscribe_to_global_task_file_changes(
384 fs.clone(),
385 TaskKind::Script,
386 paths::tasks_file().clone(),
387 cx,
388 ),
389 Self::subscribe_to_global_task_file_changes(
390 fs,
391 TaskKind::Debug,
392 paths::debug_tasks_file().clone(),
393 cx,
394 ),
395 ),
396 }
397 }
398
399 pub fn new_remote(
400 fs: Arc<dyn Fs>,
401 worktree_store: Entity<WorktreeStore>,
402 task_store: Entity<TaskStore>,
403 cx: &mut Context<Self>,
404 ) -> Self {
405 Self {
406 worktree_store,
407 task_store,
408 mode: SettingsObserverMode::Remote,
409 downstream_client: None,
410 project_id: 0,
411 _global_task_config_watchers: (
412 Self::subscribe_to_global_task_file_changes(
413 fs.clone(),
414 TaskKind::Script,
415 paths::tasks_file().clone(),
416 cx,
417 ),
418 Self::subscribe_to_global_task_file_changes(
419 fs.clone(),
420 TaskKind::Debug,
421 paths::debug_tasks_file().clone(),
422 cx,
423 ),
424 ),
425 }
426 }
427
428 pub fn shared(
429 &mut self,
430 project_id: u64,
431 downstream_client: AnyProtoClient,
432 cx: &mut Context<Self>,
433 ) {
434 self.project_id = project_id;
435 self.downstream_client = Some(downstream_client.clone());
436
437 let store = cx.global::<SettingsStore>();
438 for worktree in self.worktree_store.read(cx).worktrees() {
439 let worktree_id = worktree.read(cx).id().to_proto();
440 for (path, content) in store.local_settings(worktree.read(cx).id()) {
441 downstream_client
442 .send(proto::UpdateWorktreeSettings {
443 project_id,
444 worktree_id,
445 path: path.to_proto(),
446 content: Some(content),
447 kind: Some(
448 local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
449 ),
450 })
451 .log_err();
452 }
453 for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
454 downstream_client
455 .send(proto::UpdateWorktreeSettings {
456 project_id,
457 worktree_id,
458 path: path.to_proto(),
459 content: Some(content),
460 kind: Some(
461 local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
462 ),
463 })
464 .log_err();
465 }
466 }
467 }
468
469 pub fn unshared(&mut self, _: &mut Context<Self>) {
470 self.downstream_client = None;
471 }
472
473 async fn handle_update_worktree_settings(
474 this: Entity<Self>,
475 envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
476 mut cx: AsyncApp,
477 ) -> anyhow::Result<()> {
478 let kind = match envelope.payload.kind {
479 Some(kind) => proto::LocalSettingsKind::from_i32(kind)
480 .with_context(|| format!("unknown kind {kind}"))?,
481 None => proto::LocalSettingsKind::Settings,
482 };
483 this.update(&mut cx, |this, cx| {
484 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
485 let Some(worktree) = this
486 .worktree_store
487 .read(cx)
488 .worktree_for_id(worktree_id, cx)
489 else {
490 return;
491 };
492
493 this.update_settings(
494 worktree,
495 [(
496 Arc::<Path>::from_proto(envelope.payload.path.clone()),
497 local_settings_kind_from_proto(kind),
498 envelope.payload.content,
499 )],
500 cx,
501 );
502 })?;
503 Ok(())
504 }
505
506 fn on_worktree_store_event(
507 &mut self,
508 _: Entity<WorktreeStore>,
509 event: &WorktreeStoreEvent,
510 cx: &mut Context<Self>,
511 ) {
512 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
513 cx.subscribe(worktree, |this, worktree, event, cx| {
514 if let worktree::Event::UpdatedEntries(changes) = event {
515 this.update_local_worktree_settings(&worktree, changes, cx)
516 }
517 })
518 .detach()
519 }
520 }
521
522 fn update_local_worktree_settings(
523 &mut self,
524 worktree: &Entity<Worktree>,
525 changes: &UpdatedEntriesSet,
526 cx: &mut Context<Self>,
527 ) {
528 let SettingsObserverMode::Local(fs) = &self.mode else {
529 return;
530 };
531
532 let mut settings_contents = Vec::new();
533 for (path, _, change) in changes.iter() {
534 let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
535 let settings_dir = Arc::<Path>::from(
536 path.ancestors()
537 .nth(local_settings_file_relative_path().components().count())
538 .unwrap(),
539 );
540 (settings_dir, LocalSettingsKind::Settings)
541 } else if path.ends_with(local_tasks_file_relative_path()) {
542 let settings_dir = Arc::<Path>::from(
543 path.ancestors()
544 .nth(
545 local_tasks_file_relative_path()
546 .components()
547 .count()
548 .saturating_sub(1),
549 )
550 .unwrap(),
551 );
552 (settings_dir, LocalSettingsKind::Tasks(TaskKind::Script))
553 } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
554 let settings_dir = Arc::<Path>::from(
555 path.ancestors()
556 .nth(
557 local_vscode_tasks_file_relative_path()
558 .components()
559 .count()
560 .saturating_sub(1),
561 )
562 .unwrap(),
563 );
564 (settings_dir, LocalSettingsKind::Tasks(TaskKind::Script))
565 } else if path.ends_with(local_debug_file_relative_path()) {
566 let settings_dir = Arc::<Path>::from(
567 path.ancestors()
568 .nth(
569 local_debug_file_relative_path()
570 .components()
571 .count()
572 .saturating_sub(1),
573 )
574 .unwrap(),
575 );
576 (settings_dir, LocalSettingsKind::Tasks(TaskKind::Debug))
577 } else if path.ends_with(local_vscode_launch_file_relative_path()) {
578 let settings_dir = Arc::<Path>::from(
579 path.ancestors()
580 .nth(
581 local_vscode_tasks_file_relative_path()
582 .components()
583 .count()
584 .saturating_sub(1),
585 )
586 .unwrap(),
587 );
588 (settings_dir, LocalSettingsKind::Tasks(TaskKind::Debug))
589 } else if path.ends_with(EDITORCONFIG_NAME) {
590 let Some(settings_dir) = path.parent().map(Arc::from) else {
591 continue;
592 };
593 (settings_dir, LocalSettingsKind::Editorconfig)
594 } else {
595 continue;
596 };
597
598 let removed = change == &PathChange::Removed;
599 let fs = fs.clone();
600 let abs_path = match worktree.read(cx).absolutize(path) {
601 Ok(abs_path) => abs_path,
602 Err(e) => {
603 log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
604 continue;
605 }
606 };
607 settings_contents.push(async move {
608 (
609 settings_dir,
610 kind,
611 if removed {
612 None
613 } else {
614 Some(
615 async move {
616 let content = fs.load(&abs_path).await?;
617 if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
618 let vscode_tasks =
619 parse_json_with_comments::<VsCodeTaskFile>(&content)
620 .with_context(|| {
621 format!("parsing VSCode tasks, file {abs_path:?}")
622 })?;
623 let zed_tasks = TaskTemplates::try_from(vscode_tasks)
624 .with_context(|| {
625 format!(
626 "converting VSCode tasks into Zed ones, file {abs_path:?}"
627 )
628 })?;
629 serde_json::to_string(&zed_tasks).with_context(|| {
630 format!(
631 "serializing Zed tasks into JSON, file {abs_path:?}"
632 )
633 })
634 } else if abs_path.ends_with(local_vscode_launch_file_relative_path()) {
635 let vscode_tasks =
636 parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
637 .with_context(|| {
638 format!("parsing VSCode debug tasks, file {abs_path:?}")
639 })?;
640 let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
641 .with_context(|| {
642 format!(
643 "converting VSCode debug tasks into Zed ones, file {abs_path:?}"
644 )
645 })?;
646 serde_json::to_string(&zed_tasks).with_context(|| {
647 format!(
648 "serializing Zed tasks into JSON, file {abs_path:?}"
649 )
650 })
651 } else {
652 Ok(content)
653 }
654 }
655 .await,
656 )
657 },
658 )
659 });
660 }
661
662 if settings_contents.is_empty() {
663 return;
664 }
665
666 let worktree = worktree.clone();
667 cx.spawn(async move |this, cx| {
668 let settings_contents: Vec<(Arc<Path>, _, _)> =
669 futures::future::join_all(settings_contents).await;
670 cx.update(|cx| {
671 this.update(cx, |this, cx| {
672 this.update_settings(
673 worktree,
674 settings_contents.into_iter().map(|(path, kind, content)| {
675 (path, kind, content.and_then(|c| c.log_err()))
676 }),
677 cx,
678 )
679 })
680 })
681 })
682 .detach();
683 }
684
685 fn update_settings(
686 &mut self,
687 worktree: Entity<Worktree>,
688 settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
689 cx: &mut Context<Self>,
690 ) {
691 let worktree_id = worktree.read(cx).id();
692 let remote_worktree_id = worktree.read(cx).id();
693 let task_store = self.task_store.clone();
694
695 for (directory, kind, file_content) in settings_contents {
696 match kind {
697 LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
698 .update_global::<SettingsStore, _>(|store, cx| {
699 let result = store.set_local_settings(
700 worktree_id,
701 directory.clone(),
702 kind,
703 file_content.as_deref(),
704 cx,
705 );
706
707 match result {
708 Err(InvalidSettingsError::LocalSettings { path, message }) => {
709 log::error!("Failed to set local settings in {path:?}: {message}");
710 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
711 InvalidSettingsError::LocalSettings { path, message },
712 )));
713 }
714 Err(e) => {
715 log::error!("Failed to set local settings: {e}");
716 }
717 Ok(()) => {
718 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(
719 directory.join(local_settings_file_relative_path())
720 )));
721 }
722 }
723 }),
724 LocalSettingsKind::Tasks(task_kind) => {
725 let result = task_store.update(cx, |task_store, cx| {
726 task_store.update_user_tasks(
727 TaskSettingsLocation::Worktree(SettingsLocation {
728 worktree_id,
729 path: directory.as_ref(),
730 }),
731 file_content.as_deref(),
732 task_kind,
733 cx,
734 )
735 });
736
737 match result {
738 Err(InvalidSettingsError::Tasks { path, message }) => {
739 log::error!("Failed to set local tasks in {path:?}: {message:?}");
740 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
741 InvalidSettingsError::Tasks { path, message },
742 )));
743 }
744 Err(e) => {
745 log::error!("Failed to set local tasks: {e}");
746 }
747 Ok(()) => {
748 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
749 task_kind.config_in_dir(&directory)
750 )));
751 }
752 }
753 }
754 };
755
756 if let Some(downstream_client) = &self.downstream_client {
757 downstream_client
758 .send(proto::UpdateWorktreeSettings {
759 project_id: self.project_id,
760 worktree_id: remote_worktree_id.to_proto(),
761 path: directory.to_proto(),
762 content: file_content,
763 kind: Some(local_settings_kind_to_proto(kind).into()),
764 })
765 .log_err();
766 }
767 }
768 }
769
770 fn subscribe_to_global_task_file_changes(
771 fs: Arc<dyn Fs>,
772 task_kind: TaskKind,
773 file_path: PathBuf,
774 cx: &mut Context<Self>,
775 ) -> Task<()> {
776 let mut user_tasks_file_rx =
777 watch_config_file(&cx.background_executor(), fs, file_path.clone());
778 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
779 let weak_entry = cx.weak_entity();
780 cx.spawn(async move |settings_observer, cx| {
781 let Ok(task_store) = settings_observer.update(cx, |settings_observer, _| {
782 settings_observer.task_store.clone()
783 }) else {
784 return;
785 };
786 if let Some(user_tasks_content) = user_tasks_content {
787 let Ok(()) = task_store.update(cx, |task_store, cx| {
788 task_store
789 .update_user_tasks(
790 TaskSettingsLocation::Global(&file_path),
791 Some(&user_tasks_content),
792 task_kind,
793 cx,
794 )
795 .log_err();
796 }) else {
797 return;
798 };
799 }
800 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
801 let Ok(result) = task_store.update(cx, |task_store, cx| {
802 task_store.update_user_tasks(
803 TaskSettingsLocation::Global(&file_path),
804 Some(&user_tasks_content),
805 task_kind,
806 cx,
807 )
808 }) else {
809 break;
810 };
811
812 weak_entry
813 .update(cx, |_, cx| match result {
814 Ok(()) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
815 file_path.clone()
816 ))),
817 Err(err) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
818 InvalidSettingsError::Tasks {
819 path: file_path.clone(),
820 message: err.to_string(),
821 },
822 ))),
823 })
824 .ok();
825 }
826 })
827 }
828}
829
830pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
831 match kind {
832 proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
833 proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks(TaskKind::Script),
834 proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
835 }
836}
837
838pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
839 match kind {
840 LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
841 LocalSettingsKind::Tasks(_) => proto::LocalSettingsKind::Tasks,
842 LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
843 }
844}