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(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 /// Configuration, related to Rust language diagnostics.
113 #[serde(default)]
114 pub cargo: Option<CargoDiagnosticsSettings>,
115}
116
117impl DiagnosticsSettings {
118 pub fn fetch_cargo_diagnostics(&self) -> bool {
119 self.cargo.as_ref().map_or(false, |cargo_diagnostics| {
120 cargo_diagnostics.fetch_cargo_diagnostics
121 })
122 }
123}
124
125#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
126pub struct InlineDiagnosticsSettings {
127 /// Whether or not to show inline diagnostics
128 ///
129 /// Default: false
130 #[serde(default)]
131 pub enabled: bool,
132 /// Whether to only show the inline diagnostics after a delay after the
133 /// last editor event.
134 ///
135 /// Default: 150
136 #[serde(default = "default_inline_diagnostics_debounce_ms")]
137 pub update_debounce_ms: u64,
138 /// The amount of padding between the end of the source line and the start
139 /// of the inline diagnostic in units of columns.
140 ///
141 /// Default: 4
142 #[serde(default = "default_inline_diagnostics_padding")]
143 pub padding: u32,
144 /// The minimum column to display inline diagnostics. This setting can be
145 /// used to horizontally align inline diagnostics at some position. Lines
146 /// longer than this value will still push diagnostics further to the right.
147 ///
148 /// Default: 0
149 #[serde(default)]
150 pub min_column: u32,
151
152 #[serde(default)]
153 pub max_severity: Option<DiagnosticSeverity>,
154}
155
156#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
157pub struct CargoDiagnosticsSettings {
158 /// When enabled, Zed runs `cargo check --message-format=json`-based commands and
159 /// collect cargo diagnostics instead of rust-analyzer.
160 ///
161 /// Default: false
162 #[serde(default)]
163 pub fetch_cargo_diagnostics: bool,
164
165 /// A command override for fetching the cargo diagnostics.
166 /// First argument is the command, followed by the arguments.
167 ///
168 /// Default: ["cargo", "check", "--quiet", "--workspace", "--message-format=json", "--all-targets", "--keep-going"]
169 #[serde(default = "default_diagnostics_fetch_command")]
170 pub diagnostics_fetch_command: Vec<String>,
171
172 /// Extra environment variables to pass to the diagnostics fetch command.
173 ///
174 /// Default: {}
175 #[serde(default)]
176 pub env: HashMap<String, String>,
177}
178
179fn default_diagnostics_fetch_command() -> Vec<String> {
180 vec![
181 "cargo".to_string(),
182 "check".to_string(),
183 "--quiet".to_string(),
184 "--workspace".to_string(),
185 "--message-format=json".to_string(),
186 "--all-targets".to_string(),
187 "--keep-going".to_string(),
188 ]
189}
190
191#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
192#[serde(rename_all = "snake_case")]
193pub enum DiagnosticSeverity {
194 Error,
195 Warning,
196 Info,
197 Hint,
198}
199
200impl Default for InlineDiagnosticsSettings {
201 fn default() -> Self {
202 Self {
203 enabled: false,
204 update_debounce_ms: default_inline_diagnostics_debounce_ms(),
205 padding: default_inline_diagnostics_padding(),
206 min_column: 0,
207 max_severity: None,
208 }
209 }
210}
211
212fn default_inline_diagnostics_debounce_ms() -> u64 {
213 150
214}
215
216fn default_inline_diagnostics_padding() -> u32 {
217 4
218}
219
220#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
221pub struct GitSettings {
222 /// Whether or not to show the git gutter.
223 ///
224 /// Default: tracked_files
225 pub git_gutter: Option<GitGutterSetting>,
226 /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
227 ///
228 /// Default: null
229 pub gutter_debounce: Option<u64>,
230 /// Whether or not to show git blame data inline in
231 /// the currently focused line.
232 ///
233 /// Default: on
234 pub inline_blame: Option<InlineBlameSettings>,
235 /// How hunks are displayed visually in the editor.
236 ///
237 /// Default: staged_hollow
238 pub hunk_style: Option<GitHunkStyleSetting>,
239}
240
241impl GitSettings {
242 pub fn inline_blame_enabled(&self) -> bool {
243 #[allow(unknown_lints, clippy::manual_unwrap_or_default)]
244 match self.inline_blame {
245 Some(InlineBlameSettings { enabled, .. }) => enabled,
246 _ => false,
247 }
248 }
249
250 pub fn inline_blame_delay(&self) -> Option<Duration> {
251 match self.inline_blame {
252 Some(InlineBlameSettings {
253 delay_ms: Some(delay_ms),
254 ..
255 }) if delay_ms > 0 => Some(Duration::from_millis(delay_ms)),
256 _ => None,
257 }
258 }
259
260 pub fn show_inline_commit_summary(&self) -> bool {
261 match self.inline_blame {
262 Some(InlineBlameSettings {
263 show_commit_summary,
264 ..
265 }) => show_commit_summary,
266 _ => false,
267 }
268 }
269}
270
271#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
272#[serde(rename_all = "snake_case")]
273pub enum GitHunkStyleSetting {
274 /// Show unstaged hunks with a filled background and staged hunks hollow.
275 #[default]
276 StagedHollow,
277 /// Show unstaged hunks hollow and staged hunks with a filled background.
278 UnstagedHollow,
279}
280
281#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
282#[serde(rename_all = "snake_case")]
283pub enum GitGutterSetting {
284 /// Show git gutter in tracked files.
285 #[default]
286 TrackedFiles,
287 /// Hide git gutter
288 Hide,
289}
290
291#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
292#[serde(rename_all = "snake_case")]
293pub struct InlineBlameSettings {
294 /// Whether or not to show git blame data inline in
295 /// the currently focused line.
296 ///
297 /// Default: true
298 #[serde(default = "true_value")]
299 pub enabled: bool,
300 /// Whether to only show the inline blame information
301 /// after a delay once the cursor stops moving.
302 ///
303 /// Default: 0
304 pub delay_ms: Option<u64>,
305 /// The minimum column number to show the inline blame information at
306 ///
307 /// Default: 0
308 pub min_column: Option<u32>,
309 /// Whether to show commit summary as part of the inline blame.
310 ///
311 /// Default: false
312 #[serde(default)]
313 pub show_commit_summary: bool,
314}
315
316const fn true_value() -> bool {
317 true
318}
319
320#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
321pub struct BinarySettings {
322 pub path: Option<String>,
323 pub arguments: Option<Vec<String>>,
324 // this can't be an FxHashMap because the extension APIs require the default SipHash
325 pub env: Option<std::collections::HashMap<String, String, std::hash::RandomState>>,
326 pub ignore_system_version: Option<bool>,
327}
328
329#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
330#[serde(rename_all = "snake_case")]
331pub struct LspSettings {
332 pub binary: Option<BinarySettings>,
333 pub initialization_options: Option<serde_json::Value>,
334 pub settings: Option<serde_json::Value>,
335 /// If the server supports sending tasks over LSP extensions,
336 /// this setting can be used to enable or disable them in Zed.
337 /// Default: true
338 #[serde(default = "default_true")]
339 pub enable_lsp_tasks: bool,
340}
341
342impl Default for LspSettings {
343 fn default() -> Self {
344 Self {
345 binary: None,
346 initialization_options: None,
347 settings: None,
348 enable_lsp_tasks: true,
349 }
350 }
351}
352
353#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
354pub struct SessionSettings {
355 /// Whether or not to restore unsaved buffers on restart.
356 ///
357 /// If this is true, user won't be prompted whether to save/discard
358 /// dirty files when closing the application.
359 ///
360 /// Default: true
361 pub restore_unsaved_buffers: bool,
362}
363
364impl Default for SessionSettings {
365 fn default() -> Self {
366 Self {
367 restore_unsaved_buffers: true,
368 }
369 }
370}
371
372impl Settings for ProjectSettings {
373 const KEY: Option<&'static str> = None;
374
375 type FileContent = Self;
376
377 fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
378 sources.json_merge()
379 }
380
381 fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
382 // this just sets the binary name instead of a full path so it relies on path lookup
383 // resolving to the one you want
384 vscode.enum_setting(
385 "npm.packageManager",
386 &mut current.node.npm_path,
387 |s| match s {
388 v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
389 _ => None,
390 },
391 );
392
393 if let Some(b) = vscode.read_bool("git.blame.editorDecoration.enabled") {
394 if let Some(blame) = current.git.inline_blame.as_mut() {
395 blame.enabled = b
396 } else {
397 current.git.inline_blame = Some(InlineBlameSettings {
398 enabled: b,
399 ..Default::default()
400 })
401 }
402 }
403
404 // TODO: translate lsp settings for rust-analyzer and other popular ones to old.lsp
405 }
406}
407
408pub enum SettingsObserverMode {
409 Local(Arc<dyn Fs>),
410 Remote,
411}
412
413#[derive(Clone, Debug, PartialEq)]
414pub enum SettingsObserverEvent {
415 LocalSettingsUpdated(Result<PathBuf, InvalidSettingsError>),
416 LocalTasksUpdated(Result<PathBuf, InvalidSettingsError>),
417}
418
419impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
420
421pub struct SettingsObserver {
422 mode: SettingsObserverMode,
423 downstream_client: Option<AnyProtoClient>,
424 worktree_store: Entity<WorktreeStore>,
425 project_id: u64,
426 task_store: Entity<TaskStore>,
427 _global_task_config_watcher: Task<()>,
428}
429
430/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
431/// (or the equivalent protobuf messages from upstream) and updates local settings
432/// and sends notifications downstream.
433/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
434/// upstream.
435impl SettingsObserver {
436 pub fn init(client: &AnyProtoClient) {
437 client.add_entity_message_handler(Self::handle_update_worktree_settings);
438 }
439
440 pub fn new_local(
441 fs: Arc<dyn Fs>,
442 worktree_store: Entity<WorktreeStore>,
443 task_store: Entity<TaskStore>,
444 cx: &mut Context<Self>,
445 ) -> Self {
446 cx.subscribe(&worktree_store, Self::on_worktree_store_event)
447 .detach();
448
449 Self {
450 worktree_store,
451 task_store,
452 mode: SettingsObserverMode::Local(fs.clone()),
453 downstream_client: None,
454 project_id: 0,
455 _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
456 fs.clone(),
457 paths::tasks_file().clone(),
458 cx,
459 ),
460 }
461 }
462
463 pub fn new_remote(
464 fs: Arc<dyn Fs>,
465 worktree_store: Entity<WorktreeStore>,
466 task_store: Entity<TaskStore>,
467 cx: &mut Context<Self>,
468 ) -> Self {
469 Self {
470 worktree_store,
471 task_store,
472 mode: SettingsObserverMode::Remote,
473 downstream_client: None,
474 project_id: 0,
475 _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
476 fs.clone(),
477 paths::tasks_file().clone(),
478 cx,
479 ),
480 }
481 }
482
483 pub fn shared(
484 &mut self,
485 project_id: u64,
486 downstream_client: AnyProtoClient,
487 cx: &mut Context<Self>,
488 ) {
489 self.project_id = project_id;
490 self.downstream_client = Some(downstream_client.clone());
491
492 let store = cx.global::<SettingsStore>();
493 for worktree in self.worktree_store.read(cx).worktrees() {
494 let worktree_id = worktree.read(cx).id().to_proto();
495 for (path, content) in store.local_settings(worktree.read(cx).id()) {
496 downstream_client
497 .send(proto::UpdateWorktreeSettings {
498 project_id,
499 worktree_id,
500 path: path.to_proto(),
501 content: Some(content),
502 kind: Some(
503 local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
504 ),
505 })
506 .log_err();
507 }
508 for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
509 downstream_client
510 .send(proto::UpdateWorktreeSettings {
511 project_id,
512 worktree_id,
513 path: path.to_proto(),
514 content: Some(content),
515 kind: Some(
516 local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
517 ),
518 })
519 .log_err();
520 }
521 }
522 }
523
524 pub fn unshared(&mut self, _: &mut Context<Self>) {
525 self.downstream_client = None;
526 }
527
528 async fn handle_update_worktree_settings(
529 this: Entity<Self>,
530 envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
531 mut cx: AsyncApp,
532 ) -> anyhow::Result<()> {
533 let kind = match envelope.payload.kind {
534 Some(kind) => proto::LocalSettingsKind::from_i32(kind)
535 .with_context(|| format!("unknown kind {kind}"))?,
536 None => proto::LocalSettingsKind::Settings,
537 };
538 this.update(&mut cx, |this, cx| {
539 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
540 let Some(worktree) = this
541 .worktree_store
542 .read(cx)
543 .worktree_for_id(worktree_id, cx)
544 else {
545 return;
546 };
547
548 this.update_settings(
549 worktree,
550 [(
551 Arc::<Path>::from_proto(envelope.payload.path.clone()),
552 local_settings_kind_from_proto(kind),
553 envelope.payload.content,
554 )],
555 cx,
556 );
557 })?;
558 Ok(())
559 }
560
561 fn on_worktree_store_event(
562 &mut self,
563 _: Entity<WorktreeStore>,
564 event: &WorktreeStoreEvent,
565 cx: &mut Context<Self>,
566 ) {
567 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
568 cx.subscribe(worktree, |this, worktree, event, cx| {
569 if let worktree::Event::UpdatedEntries(changes) = event {
570 this.update_local_worktree_settings(&worktree, changes, cx)
571 }
572 })
573 .detach()
574 }
575 }
576
577 fn update_local_worktree_settings(
578 &mut self,
579 worktree: &Entity<Worktree>,
580 changes: &UpdatedEntriesSet,
581 cx: &mut Context<Self>,
582 ) {
583 let SettingsObserverMode::Local(fs) = &self.mode else {
584 return;
585 };
586
587 let mut settings_contents = Vec::new();
588 for (path, _, change) in changes.iter() {
589 let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
590 let settings_dir = Arc::<Path>::from(
591 path.ancestors()
592 .nth(local_settings_file_relative_path().components().count())
593 .unwrap(),
594 );
595 (settings_dir, LocalSettingsKind::Settings)
596 } else if path.ends_with(local_tasks_file_relative_path()) {
597 let settings_dir = Arc::<Path>::from(
598 path.ancestors()
599 .nth(
600 local_tasks_file_relative_path()
601 .components()
602 .count()
603 .saturating_sub(1),
604 )
605 .unwrap(),
606 );
607 (settings_dir, LocalSettingsKind::Tasks)
608 } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
609 let settings_dir = Arc::<Path>::from(
610 path.ancestors()
611 .nth(
612 local_vscode_tasks_file_relative_path()
613 .components()
614 .count()
615 .saturating_sub(1),
616 )
617 .unwrap(),
618 );
619 (settings_dir, LocalSettingsKind::Tasks)
620 } else if path.ends_with(local_debug_file_relative_path()) {
621 let settings_dir = Arc::<Path>::from(
622 path.ancestors()
623 .nth(
624 local_debug_file_relative_path()
625 .components()
626 .count()
627 .saturating_sub(1),
628 )
629 .unwrap(),
630 );
631 (settings_dir, LocalSettingsKind::Debug)
632 } else if path.ends_with(local_vscode_launch_file_relative_path()) {
633 let settings_dir = Arc::<Path>::from(
634 path.ancestors()
635 .nth(
636 local_vscode_tasks_file_relative_path()
637 .components()
638 .count()
639 .saturating_sub(1),
640 )
641 .unwrap(),
642 );
643 (settings_dir, LocalSettingsKind::Debug)
644 } else if path.ends_with(EDITORCONFIG_NAME) {
645 let Some(settings_dir) = path.parent().map(Arc::from) else {
646 continue;
647 };
648 (settings_dir, LocalSettingsKind::Editorconfig)
649 } else {
650 continue;
651 };
652
653 let removed = change == &PathChange::Removed;
654 let fs = fs.clone();
655 let abs_path = match worktree.read(cx).absolutize(path) {
656 Ok(abs_path) => abs_path,
657 Err(e) => {
658 log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
659 continue;
660 }
661 };
662 settings_contents.push(async move {
663 (
664 settings_dir,
665 kind,
666 if removed {
667 None
668 } else {
669 Some(
670 async move {
671 let content = fs.load(&abs_path).await?;
672 if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
673 let vscode_tasks =
674 parse_json_with_comments::<VsCodeTaskFile>(&content)
675 .with_context(|| {
676 format!("parsing VSCode tasks, file {abs_path:?}")
677 })?;
678 let zed_tasks = TaskTemplates::try_from(vscode_tasks)
679 .with_context(|| {
680 format!(
681 "converting VSCode tasks into Zed ones, file {abs_path:?}"
682 )
683 })?;
684 serde_json::to_string(&zed_tasks).with_context(|| {
685 format!(
686 "serializing Zed tasks into JSON, file {abs_path:?}"
687 )
688 })
689 } else if abs_path.ends_with(local_vscode_launch_file_relative_path()) {
690 let vscode_tasks =
691 parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
692 .with_context(|| {
693 format!("parsing VSCode debug tasks, file {abs_path:?}")
694 })?;
695 let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
696 .with_context(|| {
697 format!(
698 "converting VSCode debug tasks into Zed ones, file {abs_path:?}"
699 )
700 })?;
701 serde_json::to_string(&zed_tasks).with_context(|| {
702 format!(
703 "serializing Zed tasks into JSON, file {abs_path:?}"
704 )
705 })
706 } else {
707 Ok(content)
708 }
709 }
710 .await,
711 )
712 },
713 )
714 });
715 }
716
717 if settings_contents.is_empty() {
718 return;
719 }
720
721 let worktree = worktree.clone();
722 cx.spawn(async move |this, cx| {
723 let settings_contents: Vec<(Arc<Path>, _, _)> =
724 futures::future::join_all(settings_contents).await;
725 cx.update(|cx| {
726 this.update(cx, |this, cx| {
727 this.update_settings(
728 worktree,
729 settings_contents.into_iter().map(|(path, kind, content)| {
730 (path, kind, content.and_then(|c| c.log_err()))
731 }),
732 cx,
733 )
734 })
735 })
736 })
737 .detach();
738 }
739
740 fn update_settings(
741 &mut self,
742 worktree: Entity<Worktree>,
743 settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
744 cx: &mut Context<Self>,
745 ) {
746 let worktree_id = worktree.read(cx).id();
747 let remote_worktree_id = worktree.read(cx).id();
748 let task_store = self.task_store.clone();
749
750 for (directory, kind, file_content) in settings_contents {
751 match kind {
752 LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
753 .update_global::<SettingsStore, _>(|store, cx| {
754 let result = store.set_local_settings(
755 worktree_id,
756 directory.clone(),
757 kind,
758 file_content.as_deref(),
759 cx,
760 );
761
762 match result {
763 Err(InvalidSettingsError::LocalSettings { path, message }) => {
764 log::error!("Failed to set local settings in {path:?}: {message}");
765 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
766 InvalidSettingsError::LocalSettings { path, message },
767 )));
768 }
769 Err(e) => {
770 log::error!("Failed to set local settings: {e}");
771 }
772 Ok(()) => {
773 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(
774 directory.join(local_settings_file_relative_path())
775 )));
776 }
777 }
778 }),
779 LocalSettingsKind::Tasks => {
780 let result = task_store.update(cx, |task_store, cx| {
781 task_store.update_user_tasks(
782 TaskSettingsLocation::Worktree(SettingsLocation {
783 worktree_id,
784 path: directory.as_ref(),
785 }),
786 file_content.as_deref(),
787 cx,
788 )
789 });
790
791 match result {
792 Err(InvalidSettingsError::Tasks { path, message }) => {
793 log::error!("Failed to set local tasks in {path:?}: {message:?}");
794 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
795 InvalidSettingsError::Tasks { path, message },
796 )));
797 }
798 Err(e) => {
799 log::error!("Failed to set local tasks: {e}");
800 }
801 Ok(()) => {
802 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
803 directory.join(task_file_name())
804 )));
805 }
806 }
807 }
808 LocalSettingsKind::Debug => {
809 let result = task_store.update(cx, |task_store, cx| {
810 task_store.update_user_debug_scenarios(
811 TaskSettingsLocation::Worktree(SettingsLocation {
812 worktree_id,
813 path: directory.as_ref(),
814 }),
815 file_content.as_deref(),
816 cx,
817 )
818 });
819
820 match result {
821 Err(InvalidSettingsError::Debug { path, message }) => {
822 log::error!(
823 "Failed to set local debug scenarios in {path:?}: {message:?}"
824 );
825 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
826 InvalidSettingsError::Debug { path, message },
827 )));
828 }
829 Err(e) => {
830 log::error!("Failed to set local tasks: {e}");
831 }
832 Ok(()) => {
833 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
834 directory.join(task_file_name())
835 )));
836 }
837 }
838 }
839 };
840
841 if let Some(downstream_client) = &self.downstream_client {
842 downstream_client
843 .send(proto::UpdateWorktreeSettings {
844 project_id: self.project_id,
845 worktree_id: remote_worktree_id.to_proto(),
846 path: directory.to_proto(),
847 content: file_content,
848 kind: Some(local_settings_kind_to_proto(kind).into()),
849 })
850 .log_err();
851 }
852 }
853 }
854
855 fn subscribe_to_global_task_file_changes(
856 fs: Arc<dyn Fs>,
857 file_path: PathBuf,
858 cx: &mut Context<Self>,
859 ) -> Task<()> {
860 let mut user_tasks_file_rx =
861 watch_config_file(&cx.background_executor(), fs, file_path.clone());
862 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
863 let weak_entry = cx.weak_entity();
864 cx.spawn(async move |settings_observer, cx| {
865 let Ok(task_store) = settings_observer.update(cx, |settings_observer, _| {
866 settings_observer.task_store.clone()
867 }) else {
868 return;
869 };
870 if let Some(user_tasks_content) = user_tasks_content {
871 let Ok(()) = task_store.update(cx, |task_store, cx| {
872 task_store
873 .update_user_tasks(
874 TaskSettingsLocation::Global(&file_path),
875 Some(&user_tasks_content),
876 cx,
877 )
878 .log_err();
879 }) else {
880 return;
881 };
882 }
883 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
884 let Ok(result) = task_store.update(cx, |task_store, cx| {
885 task_store.update_user_tasks(
886 TaskSettingsLocation::Global(&file_path),
887 Some(&user_tasks_content),
888 cx,
889 )
890 }) else {
891 break;
892 };
893
894 weak_entry
895 .update(cx, |_, cx| match result {
896 Ok(()) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
897 file_path.clone()
898 ))),
899 Err(err) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
900 InvalidSettingsError::Tasks {
901 path: file_path.clone(),
902 message: err.to_string(),
903 },
904 ))),
905 })
906 .ok();
907 }
908 })
909 }
910}
911
912pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
913 match kind {
914 proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
915 proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
916 proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
917 proto::LocalSettingsKind::Debug => LocalSettingsKind::Debug,
918 }
919}
920
921pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
922 match kind {
923 LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
924 LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
925 LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
926 LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug,
927 }
928}