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 enabled, 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 fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
335 // this just sets the binary name instead of a full path so it relies on path lookup
336 // resolving to the one you want
337 vscode.enum_setting(
338 "npm.packageManager",
339 &mut current.node.npm_path,
340 |s| match s {
341 v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
342 _ => None,
343 },
344 );
345
346 if let Some(b) = vscode.read_bool("git.blame.editorDecoration.enabled") {
347 if let Some(blame) = current.git.inline_blame.as_mut() {
348 blame.enabled = b
349 } else {
350 current.git.inline_blame = Some(InlineBlameSettings {
351 enabled: b,
352 ..Default::default()
353 })
354 }
355 }
356
357 // TODO: translate lsp settings for rust-analyzer and other popular ones to old.lsp
358 }
359}
360
361pub enum SettingsObserverMode {
362 Local(Arc<dyn Fs>),
363 Remote,
364}
365
366#[derive(Clone, Debug, PartialEq)]
367pub enum SettingsObserverEvent {
368 LocalSettingsUpdated(Result<PathBuf, InvalidSettingsError>),
369 LocalTasksUpdated(Result<PathBuf, InvalidSettingsError>),
370}
371
372impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
373
374pub struct SettingsObserver {
375 mode: SettingsObserverMode,
376 downstream_client: Option<AnyProtoClient>,
377 worktree_store: Entity<WorktreeStore>,
378 project_id: u64,
379 task_store: Entity<TaskStore>,
380 _global_task_config_watchers: (Task<()>, Task<()>),
381}
382
383/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
384/// (or the equivalent protobuf messages from upstream) and updates local settings
385/// and sends notifications downstream.
386/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
387/// upstream.
388impl SettingsObserver {
389 pub fn init(client: &AnyProtoClient) {
390 client.add_entity_message_handler(Self::handle_update_worktree_settings);
391 }
392
393 pub fn new_local(
394 fs: Arc<dyn Fs>,
395 worktree_store: Entity<WorktreeStore>,
396 task_store: Entity<TaskStore>,
397 cx: &mut Context<Self>,
398 ) -> Self {
399 cx.subscribe(&worktree_store, Self::on_worktree_store_event)
400 .detach();
401
402 Self {
403 worktree_store,
404 task_store,
405 mode: SettingsObserverMode::Local(fs.clone()),
406 downstream_client: None,
407 project_id: 0,
408 _global_task_config_watchers: (
409 Self::subscribe_to_global_task_file_changes(
410 fs.clone(),
411 TaskKind::Script,
412 paths::tasks_file().clone(),
413 cx,
414 ),
415 Self::subscribe_to_global_task_file_changes(
416 fs,
417 TaskKind::Debug,
418 paths::debug_tasks_file().clone(),
419 cx,
420 ),
421 ),
422 }
423 }
424
425 pub fn new_remote(
426 fs: Arc<dyn Fs>,
427 worktree_store: Entity<WorktreeStore>,
428 task_store: Entity<TaskStore>,
429 cx: &mut Context<Self>,
430 ) -> Self {
431 Self {
432 worktree_store,
433 task_store,
434 mode: SettingsObserverMode::Remote,
435 downstream_client: None,
436 project_id: 0,
437 _global_task_config_watchers: (
438 Self::subscribe_to_global_task_file_changes(
439 fs.clone(),
440 TaskKind::Script,
441 paths::tasks_file().clone(),
442 cx,
443 ),
444 Self::subscribe_to_global_task_file_changes(
445 fs.clone(),
446 TaskKind::Debug,
447 paths::debug_tasks_file().clone(),
448 cx,
449 ),
450 ),
451 }
452 }
453
454 pub fn shared(
455 &mut self,
456 project_id: u64,
457 downstream_client: AnyProtoClient,
458 cx: &mut Context<Self>,
459 ) {
460 self.project_id = project_id;
461 self.downstream_client = Some(downstream_client.clone());
462
463 let store = cx.global::<SettingsStore>();
464 for worktree in self.worktree_store.read(cx).worktrees() {
465 let worktree_id = worktree.read(cx).id().to_proto();
466 for (path, content) in store.local_settings(worktree.read(cx).id()) {
467 downstream_client
468 .send(proto::UpdateWorktreeSettings {
469 project_id,
470 worktree_id,
471 path: path.to_proto(),
472 content: Some(content),
473 kind: Some(
474 local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
475 ),
476 })
477 .log_err();
478 }
479 for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
480 downstream_client
481 .send(proto::UpdateWorktreeSettings {
482 project_id,
483 worktree_id,
484 path: path.to_proto(),
485 content: Some(content),
486 kind: Some(
487 local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
488 ),
489 })
490 .log_err();
491 }
492 }
493 }
494
495 pub fn unshared(&mut self, _: &mut Context<Self>) {
496 self.downstream_client = None;
497 }
498
499 async fn handle_update_worktree_settings(
500 this: Entity<Self>,
501 envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
502 mut cx: AsyncApp,
503 ) -> anyhow::Result<()> {
504 let kind = match envelope.payload.kind {
505 Some(kind) => proto::LocalSettingsKind::from_i32(kind)
506 .with_context(|| format!("unknown kind {kind}"))?,
507 None => proto::LocalSettingsKind::Settings,
508 };
509 this.update(&mut cx, |this, cx| {
510 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
511 let Some(worktree) = this
512 .worktree_store
513 .read(cx)
514 .worktree_for_id(worktree_id, cx)
515 else {
516 return;
517 };
518
519 this.update_settings(
520 worktree,
521 [(
522 Arc::<Path>::from_proto(envelope.payload.path.clone()),
523 local_settings_kind_from_proto(kind),
524 envelope.payload.content,
525 )],
526 cx,
527 );
528 })?;
529 Ok(())
530 }
531
532 fn on_worktree_store_event(
533 &mut self,
534 _: Entity<WorktreeStore>,
535 event: &WorktreeStoreEvent,
536 cx: &mut Context<Self>,
537 ) {
538 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
539 cx.subscribe(worktree, |this, worktree, event, cx| {
540 if let worktree::Event::UpdatedEntries(changes) = event {
541 this.update_local_worktree_settings(&worktree, changes, cx)
542 }
543 })
544 .detach()
545 }
546 }
547
548 fn update_local_worktree_settings(
549 &mut self,
550 worktree: &Entity<Worktree>,
551 changes: &UpdatedEntriesSet,
552 cx: &mut Context<Self>,
553 ) {
554 let SettingsObserverMode::Local(fs) = &self.mode else {
555 return;
556 };
557
558 let mut settings_contents = Vec::new();
559 for (path, _, change) in changes.iter() {
560 let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
561 let settings_dir = Arc::<Path>::from(
562 path.ancestors()
563 .nth(local_settings_file_relative_path().components().count())
564 .unwrap(),
565 );
566 (settings_dir, LocalSettingsKind::Settings)
567 } else if path.ends_with(local_tasks_file_relative_path()) {
568 let settings_dir = Arc::<Path>::from(
569 path.ancestors()
570 .nth(
571 local_tasks_file_relative_path()
572 .components()
573 .count()
574 .saturating_sub(1),
575 )
576 .unwrap(),
577 );
578 (settings_dir, LocalSettingsKind::Tasks(TaskKind::Script))
579 } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
580 let settings_dir = Arc::<Path>::from(
581 path.ancestors()
582 .nth(
583 local_vscode_tasks_file_relative_path()
584 .components()
585 .count()
586 .saturating_sub(1),
587 )
588 .unwrap(),
589 );
590 (settings_dir, LocalSettingsKind::Tasks(TaskKind::Script))
591 } else if path.ends_with(local_debug_file_relative_path()) {
592 let settings_dir = Arc::<Path>::from(
593 path.ancestors()
594 .nth(
595 local_debug_file_relative_path()
596 .components()
597 .count()
598 .saturating_sub(1),
599 )
600 .unwrap(),
601 );
602 (settings_dir, LocalSettingsKind::Tasks(TaskKind::Debug))
603 } else if path.ends_with(local_vscode_launch_file_relative_path()) {
604 let settings_dir = Arc::<Path>::from(
605 path.ancestors()
606 .nth(
607 local_vscode_tasks_file_relative_path()
608 .components()
609 .count()
610 .saturating_sub(1),
611 )
612 .unwrap(),
613 );
614 (settings_dir, LocalSettingsKind::Tasks(TaskKind::Debug))
615 } else if path.ends_with(EDITORCONFIG_NAME) {
616 let Some(settings_dir) = path.parent().map(Arc::from) else {
617 continue;
618 };
619 (settings_dir, LocalSettingsKind::Editorconfig)
620 } else {
621 continue;
622 };
623
624 let removed = change == &PathChange::Removed;
625 let fs = fs.clone();
626 let abs_path = match worktree.read(cx).absolutize(path) {
627 Ok(abs_path) => abs_path,
628 Err(e) => {
629 log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
630 continue;
631 }
632 };
633 settings_contents.push(async move {
634 (
635 settings_dir,
636 kind,
637 if removed {
638 None
639 } else {
640 Some(
641 async move {
642 let content = fs.load(&abs_path).await?;
643 if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
644 let vscode_tasks =
645 parse_json_with_comments::<VsCodeTaskFile>(&content)
646 .with_context(|| {
647 format!("parsing VSCode tasks, file {abs_path:?}")
648 })?;
649 let zed_tasks = TaskTemplates::try_from(vscode_tasks)
650 .with_context(|| {
651 format!(
652 "converting VSCode tasks into Zed ones, file {abs_path:?}"
653 )
654 })?;
655 serde_json::to_string(&zed_tasks).with_context(|| {
656 format!(
657 "serializing Zed tasks into JSON, file {abs_path:?}"
658 )
659 })
660 } else if abs_path.ends_with(local_vscode_launch_file_relative_path()) {
661 let vscode_tasks =
662 parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
663 .with_context(|| {
664 format!("parsing VSCode debug tasks, file {abs_path:?}")
665 })?;
666 let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
667 .with_context(|| {
668 format!(
669 "converting VSCode debug tasks into Zed ones, file {abs_path:?}"
670 )
671 })?;
672 serde_json::to_string(&zed_tasks).with_context(|| {
673 format!(
674 "serializing Zed tasks into JSON, file {abs_path:?}"
675 )
676 })
677 } else {
678 Ok(content)
679 }
680 }
681 .await,
682 )
683 },
684 )
685 });
686 }
687
688 if settings_contents.is_empty() {
689 return;
690 }
691
692 let worktree = worktree.clone();
693 cx.spawn(async move |this, cx| {
694 let settings_contents: Vec<(Arc<Path>, _, _)> =
695 futures::future::join_all(settings_contents).await;
696 cx.update(|cx| {
697 this.update(cx, |this, cx| {
698 this.update_settings(
699 worktree,
700 settings_contents.into_iter().map(|(path, kind, content)| {
701 (path, kind, content.and_then(|c| c.log_err()))
702 }),
703 cx,
704 )
705 })
706 })
707 })
708 .detach();
709 }
710
711 fn update_settings(
712 &mut self,
713 worktree: Entity<Worktree>,
714 settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
715 cx: &mut Context<Self>,
716 ) {
717 let worktree_id = worktree.read(cx).id();
718 let remote_worktree_id = worktree.read(cx).id();
719 let task_store = self.task_store.clone();
720
721 for (directory, kind, file_content) in settings_contents {
722 match kind {
723 LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
724 .update_global::<SettingsStore, _>(|store, cx| {
725 let result = store.set_local_settings(
726 worktree_id,
727 directory.clone(),
728 kind,
729 file_content.as_deref(),
730 cx,
731 );
732
733 match result {
734 Err(InvalidSettingsError::LocalSettings { path, message }) => {
735 log::error!("Failed to set local settings in {path:?}: {message}");
736 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
737 InvalidSettingsError::LocalSettings { path, message },
738 )));
739 }
740 Err(e) => {
741 log::error!("Failed to set local settings: {e}");
742 }
743 Ok(()) => {
744 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(
745 directory.join(local_settings_file_relative_path())
746 )));
747 }
748 }
749 }),
750 LocalSettingsKind::Tasks(task_kind) => {
751 let result = task_store.update(cx, |task_store, cx| {
752 task_store.update_user_tasks(
753 TaskSettingsLocation::Worktree(SettingsLocation {
754 worktree_id,
755 path: directory.as_ref(),
756 }),
757 file_content.as_deref(),
758 task_kind,
759 cx,
760 )
761 });
762
763 match result {
764 Err(InvalidSettingsError::Tasks { path, message }) => {
765 log::error!("Failed to set local tasks in {path:?}: {message:?}");
766 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
767 InvalidSettingsError::Tasks { path, message },
768 )));
769 }
770 Err(e) => {
771 log::error!("Failed to set local tasks: {e}");
772 }
773 Ok(()) => {
774 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
775 task_kind.config_in_dir(&directory)
776 )));
777 }
778 }
779 }
780 };
781
782 if let Some(downstream_client) = &self.downstream_client {
783 downstream_client
784 .send(proto::UpdateWorktreeSettings {
785 project_id: self.project_id,
786 worktree_id: remote_worktree_id.to_proto(),
787 path: directory.to_proto(),
788 content: file_content,
789 kind: Some(local_settings_kind_to_proto(kind).into()),
790 })
791 .log_err();
792 }
793 }
794 }
795
796 fn subscribe_to_global_task_file_changes(
797 fs: Arc<dyn Fs>,
798 task_kind: TaskKind,
799 file_path: PathBuf,
800 cx: &mut Context<Self>,
801 ) -> Task<()> {
802 let mut user_tasks_file_rx =
803 watch_config_file(&cx.background_executor(), fs, file_path.clone());
804 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
805 let weak_entry = cx.weak_entity();
806 cx.spawn(async move |settings_observer, cx| {
807 let Ok(task_store) = settings_observer.update(cx, |settings_observer, _| {
808 settings_observer.task_store.clone()
809 }) else {
810 return;
811 };
812 if let Some(user_tasks_content) = user_tasks_content {
813 let Ok(()) = task_store.update(cx, |task_store, cx| {
814 task_store
815 .update_user_tasks(
816 TaskSettingsLocation::Global(&file_path),
817 Some(&user_tasks_content),
818 task_kind,
819 cx,
820 )
821 .log_err();
822 }) else {
823 return;
824 };
825 }
826 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
827 let Ok(result) = task_store.update(cx, |task_store, cx| {
828 task_store.update_user_tasks(
829 TaskSettingsLocation::Global(&file_path),
830 Some(&user_tasks_content),
831 task_kind,
832 cx,
833 )
834 }) else {
835 break;
836 };
837
838 weak_entry
839 .update(cx, |_, cx| match result {
840 Ok(()) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
841 file_path.clone()
842 ))),
843 Err(err) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
844 InvalidSettingsError::Tasks {
845 path: file_path.clone(),
846 message: err.to_string(),
847 },
848 ))),
849 })
850 .ok();
851 }
852 })
853 }
854}
855
856pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
857 match kind {
858 proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
859 proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks(TaskKind::Script),
860 proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
861 }
862}
863
864pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
865 match kind {
866 LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
867 LocalSettingsKind::Tasks(_) => proto::LocalSettingsKind::Tasks,
868 LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
869 }
870}