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, task_file_name,
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, 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_watcher: 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_watcher: Self::subscribe_to_global_task_file_changes(
409 fs.clone(),
410 paths::tasks_file().clone(),
411 cx,
412 ),
413 }
414 }
415
416 pub fn new_remote(
417 fs: Arc<dyn Fs>,
418 worktree_store: Entity<WorktreeStore>,
419 task_store: Entity<TaskStore>,
420 cx: &mut Context<Self>,
421 ) -> Self {
422 Self {
423 worktree_store,
424 task_store,
425 mode: SettingsObserverMode::Remote,
426 downstream_client: None,
427 project_id: 0,
428 _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
429 fs.clone(),
430 paths::tasks_file().clone(),
431 cx,
432 ),
433 }
434 }
435
436 pub fn shared(
437 &mut self,
438 project_id: u64,
439 downstream_client: AnyProtoClient,
440 cx: &mut Context<Self>,
441 ) {
442 self.project_id = project_id;
443 self.downstream_client = Some(downstream_client.clone());
444
445 let store = cx.global::<SettingsStore>();
446 for worktree in self.worktree_store.read(cx).worktrees() {
447 let worktree_id = worktree.read(cx).id().to_proto();
448 for (path, content) in store.local_settings(worktree.read(cx).id()) {
449 downstream_client
450 .send(proto::UpdateWorktreeSettings {
451 project_id,
452 worktree_id,
453 path: path.to_proto(),
454 content: Some(content),
455 kind: Some(
456 local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
457 ),
458 })
459 .log_err();
460 }
461 for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
462 downstream_client
463 .send(proto::UpdateWorktreeSettings {
464 project_id,
465 worktree_id,
466 path: path.to_proto(),
467 content: Some(content),
468 kind: Some(
469 local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
470 ),
471 })
472 .log_err();
473 }
474 }
475 }
476
477 pub fn unshared(&mut self, _: &mut Context<Self>) {
478 self.downstream_client = None;
479 }
480
481 async fn handle_update_worktree_settings(
482 this: Entity<Self>,
483 envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
484 mut cx: AsyncApp,
485 ) -> anyhow::Result<()> {
486 let kind = match envelope.payload.kind {
487 Some(kind) => proto::LocalSettingsKind::from_i32(kind)
488 .with_context(|| format!("unknown kind {kind}"))?,
489 None => proto::LocalSettingsKind::Settings,
490 };
491 this.update(&mut cx, |this, cx| {
492 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
493 let Some(worktree) = this
494 .worktree_store
495 .read(cx)
496 .worktree_for_id(worktree_id, cx)
497 else {
498 return;
499 };
500
501 this.update_settings(
502 worktree,
503 [(
504 Arc::<Path>::from_proto(envelope.payload.path.clone()),
505 local_settings_kind_from_proto(kind),
506 envelope.payload.content,
507 )],
508 cx,
509 );
510 })?;
511 Ok(())
512 }
513
514 fn on_worktree_store_event(
515 &mut self,
516 _: Entity<WorktreeStore>,
517 event: &WorktreeStoreEvent,
518 cx: &mut Context<Self>,
519 ) {
520 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
521 cx.subscribe(worktree, |this, worktree, event, cx| {
522 if let worktree::Event::UpdatedEntries(changes) = event {
523 this.update_local_worktree_settings(&worktree, changes, cx)
524 }
525 })
526 .detach()
527 }
528 }
529
530 fn update_local_worktree_settings(
531 &mut self,
532 worktree: &Entity<Worktree>,
533 changes: &UpdatedEntriesSet,
534 cx: &mut Context<Self>,
535 ) {
536 let SettingsObserverMode::Local(fs) = &self.mode else {
537 return;
538 };
539
540 let mut settings_contents = Vec::new();
541 for (path, _, change) in changes.iter() {
542 let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
543 let settings_dir = Arc::<Path>::from(
544 path.ancestors()
545 .nth(local_settings_file_relative_path().components().count())
546 .unwrap(),
547 );
548 (settings_dir, LocalSettingsKind::Settings)
549 } else if path.ends_with(local_tasks_file_relative_path()) {
550 let settings_dir = Arc::<Path>::from(
551 path.ancestors()
552 .nth(
553 local_tasks_file_relative_path()
554 .components()
555 .count()
556 .saturating_sub(1),
557 )
558 .unwrap(),
559 );
560 (settings_dir, LocalSettingsKind::Tasks)
561 } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
562 let settings_dir = Arc::<Path>::from(
563 path.ancestors()
564 .nth(
565 local_vscode_tasks_file_relative_path()
566 .components()
567 .count()
568 .saturating_sub(1),
569 )
570 .unwrap(),
571 );
572 (settings_dir, LocalSettingsKind::Tasks)
573 } else if path.ends_with(local_debug_file_relative_path()) {
574 let settings_dir = Arc::<Path>::from(
575 path.ancestors()
576 .nth(
577 local_debug_file_relative_path()
578 .components()
579 .count()
580 .saturating_sub(1),
581 )
582 .unwrap(),
583 );
584 (settings_dir, LocalSettingsKind::Debug)
585 } else if path.ends_with(local_vscode_launch_file_relative_path()) {
586 let settings_dir = Arc::<Path>::from(
587 path.ancestors()
588 .nth(
589 local_vscode_tasks_file_relative_path()
590 .components()
591 .count()
592 .saturating_sub(1),
593 )
594 .unwrap(),
595 );
596 (settings_dir, LocalSettingsKind::Debug)
597 } else if path.ends_with(EDITORCONFIG_NAME) {
598 let Some(settings_dir) = path.parent().map(Arc::from) else {
599 continue;
600 };
601 (settings_dir, LocalSettingsKind::Editorconfig)
602 } else {
603 continue;
604 };
605
606 let removed = change == &PathChange::Removed;
607 let fs = fs.clone();
608 let abs_path = match worktree.read(cx).absolutize(path) {
609 Ok(abs_path) => abs_path,
610 Err(e) => {
611 log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
612 continue;
613 }
614 };
615 settings_contents.push(async move {
616 (
617 settings_dir,
618 kind,
619 if removed {
620 None
621 } else {
622 Some(
623 async move {
624 let content = fs.load(&abs_path).await?;
625 if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
626 let vscode_tasks =
627 parse_json_with_comments::<VsCodeTaskFile>(&content)
628 .with_context(|| {
629 format!("parsing VSCode tasks, file {abs_path:?}")
630 })?;
631 let zed_tasks = TaskTemplates::try_from(vscode_tasks)
632 .with_context(|| {
633 format!(
634 "converting VSCode tasks into Zed ones, file {abs_path:?}"
635 )
636 })?;
637 serde_json::to_string(&zed_tasks).with_context(|| {
638 format!(
639 "serializing Zed tasks into JSON, file {abs_path:?}"
640 )
641 })
642 } else if abs_path.ends_with(local_vscode_launch_file_relative_path()) {
643 let vscode_tasks =
644 parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
645 .with_context(|| {
646 format!("parsing VSCode debug tasks, file {abs_path:?}")
647 })?;
648 let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
649 .with_context(|| {
650 format!(
651 "converting VSCode debug tasks into Zed ones, file {abs_path:?}"
652 )
653 })?;
654 serde_json::to_string(&zed_tasks).with_context(|| {
655 format!(
656 "serializing Zed tasks into JSON, file {abs_path:?}"
657 )
658 })
659 } else {
660 Ok(content)
661 }
662 }
663 .await,
664 )
665 },
666 )
667 });
668 }
669
670 if settings_contents.is_empty() {
671 return;
672 }
673
674 let worktree = worktree.clone();
675 cx.spawn(async move |this, cx| {
676 let settings_contents: Vec<(Arc<Path>, _, _)> =
677 futures::future::join_all(settings_contents).await;
678 cx.update(|cx| {
679 this.update(cx, |this, cx| {
680 this.update_settings(
681 worktree,
682 settings_contents.into_iter().map(|(path, kind, content)| {
683 (path, kind, content.and_then(|c| c.log_err()))
684 }),
685 cx,
686 )
687 })
688 })
689 })
690 .detach();
691 }
692
693 fn update_settings(
694 &mut self,
695 worktree: Entity<Worktree>,
696 settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
697 cx: &mut Context<Self>,
698 ) {
699 let worktree_id = worktree.read(cx).id();
700 let remote_worktree_id = worktree.read(cx).id();
701 let task_store = self.task_store.clone();
702
703 for (directory, kind, file_content) in settings_contents {
704 match kind {
705 LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
706 .update_global::<SettingsStore, _>(|store, cx| {
707 let result = store.set_local_settings(
708 worktree_id,
709 directory.clone(),
710 kind,
711 file_content.as_deref(),
712 cx,
713 );
714
715 match result {
716 Err(InvalidSettingsError::LocalSettings { path, message }) => {
717 log::error!("Failed to set local settings in {path:?}: {message}");
718 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
719 InvalidSettingsError::LocalSettings { path, message },
720 )));
721 }
722 Err(e) => {
723 log::error!("Failed to set local settings: {e}");
724 }
725 Ok(()) => {
726 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(
727 directory.join(local_settings_file_relative_path())
728 )));
729 }
730 }
731 }),
732 LocalSettingsKind::Tasks => {
733 let result = task_store.update(cx, |task_store, cx| {
734 task_store.update_user_tasks(
735 TaskSettingsLocation::Worktree(SettingsLocation {
736 worktree_id,
737 path: directory.as_ref(),
738 }),
739 file_content.as_deref(),
740 cx,
741 )
742 });
743
744 match result {
745 Err(InvalidSettingsError::Tasks { path, message }) => {
746 log::error!("Failed to set local tasks in {path:?}: {message:?}");
747 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
748 InvalidSettingsError::Tasks { path, message },
749 )));
750 }
751 Err(e) => {
752 log::error!("Failed to set local tasks: {e}");
753 }
754 Ok(()) => {
755 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
756 directory.join(task_file_name())
757 )));
758 }
759 }
760 }
761 LocalSettingsKind::Debug => {
762 let result = task_store.update(cx, |task_store, cx| {
763 task_store.update_user_debug_scenarios(
764 TaskSettingsLocation::Worktree(SettingsLocation {
765 worktree_id,
766 path: directory.as_ref(),
767 }),
768 file_content.as_deref(),
769 cx,
770 )
771 });
772
773 match result {
774 Err(InvalidSettingsError::Debug { path, message }) => {
775 log::error!(
776 "Failed to set local debug scenarios in {path:?}: {message:?}"
777 );
778 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
779 InvalidSettingsError::Debug { path, message },
780 )));
781 }
782 Err(e) => {
783 log::error!("Failed to set local tasks: {e}");
784 }
785 Ok(()) => {
786 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
787 directory.join(task_file_name())
788 )));
789 }
790 }
791 }
792 };
793
794 if let Some(downstream_client) = &self.downstream_client {
795 downstream_client
796 .send(proto::UpdateWorktreeSettings {
797 project_id: self.project_id,
798 worktree_id: remote_worktree_id.to_proto(),
799 path: directory.to_proto(),
800 content: file_content,
801 kind: Some(local_settings_kind_to_proto(kind).into()),
802 })
803 .log_err();
804 }
805 }
806 }
807
808 fn subscribe_to_global_task_file_changes(
809 fs: Arc<dyn Fs>,
810 file_path: PathBuf,
811 cx: &mut Context<Self>,
812 ) -> Task<()> {
813 let mut user_tasks_file_rx =
814 watch_config_file(&cx.background_executor(), fs, file_path.clone());
815 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
816 let weak_entry = cx.weak_entity();
817 cx.spawn(async move |settings_observer, cx| {
818 let Ok(task_store) = settings_observer.update(cx, |settings_observer, _| {
819 settings_observer.task_store.clone()
820 }) else {
821 return;
822 };
823 if let Some(user_tasks_content) = user_tasks_content {
824 let Ok(()) = task_store.update(cx, |task_store, cx| {
825 task_store
826 .update_user_tasks(
827 TaskSettingsLocation::Global(&file_path),
828 Some(&user_tasks_content),
829 cx,
830 )
831 .log_err();
832 }) else {
833 return;
834 };
835 }
836 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
837 let Ok(result) = task_store.update(cx, |task_store, cx| {
838 task_store.update_user_tasks(
839 TaskSettingsLocation::Global(&file_path),
840 Some(&user_tasks_content),
841 cx,
842 )
843 }) else {
844 break;
845 };
846
847 weak_entry
848 .update(cx, |_, cx| match result {
849 Ok(()) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
850 file_path.clone()
851 ))),
852 Err(err) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
853 InvalidSettingsError::Tasks {
854 path: file_path.clone(),
855 message: err.to_string(),
856 },
857 ))),
858 })
859 .ok();
860 }
861 })
862 }
863}
864
865pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
866 match kind {
867 proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
868 proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
869 proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
870 proto::LocalSettingsKind::Debug => LocalSettingsKind::Debug,
871 }
872}
873
874pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
875 match kind {
876 LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
877 LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
878 LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
879 LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug,
880 }
881}