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