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