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 local_debug_file_relative_path, local_settings_file_relative_path,
10 local_tasks_file_relative_path, local_vscode_tasks_file_relative_path, EDITORCONFIG_NAME,
11};
12use rpc::{
13 proto::{self, FromProto, ToProto},
14 AnyProtoClient, TypedEnvelope,
15};
16use schemars::JsonSchema;
17use serde::{Deserialize, Serialize};
18use settings::{
19 parse_json_with_comments, watch_config_file, InvalidSettingsError, LocalSettingsKind, Settings,
20 SettingsLocation, SettingsSources, SettingsStore, TaskKind,
21};
22use std::{
23 path::{Path, PathBuf},
24 sync::Arc,
25 time::Duration,
26};
27use task::{TaskTemplates, VsCodeTaskFile};
28use util::ResultExt;
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, Default, 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}
288
289#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
290pub struct SessionSettings {
291 /// Whether or not to restore unsaved buffers on restart.
292 ///
293 /// If this is true, user won't be prompted whether to save/discard
294 /// dirty files when closing the application.
295 ///
296 /// Default: true
297 pub restore_unsaved_buffers: bool,
298}
299
300impl Default for SessionSettings {
301 fn default() -> Self {
302 Self {
303 restore_unsaved_buffers: true,
304 }
305 }
306}
307
308impl Settings for ProjectSettings {
309 const KEY: Option<&'static str> = None;
310
311 type FileContent = Self;
312
313 fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
314 sources.json_merge()
315 }
316}
317
318pub enum SettingsObserverMode {
319 Local(Arc<dyn Fs>),
320 Remote,
321}
322
323#[derive(Clone, Debug, PartialEq)]
324pub enum SettingsObserverEvent {
325 LocalSettingsUpdated(Result<PathBuf, InvalidSettingsError>),
326 LocalTasksUpdated(Result<PathBuf, InvalidSettingsError>),
327}
328
329impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
330
331pub struct SettingsObserver {
332 mode: SettingsObserverMode,
333 downstream_client: Option<AnyProtoClient>,
334 worktree_store: Entity<WorktreeStore>,
335 project_id: u64,
336 task_store: Entity<TaskStore>,
337 _global_task_config_watchers: (Task<()>, Task<()>),
338}
339
340/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
341/// (or the equivalent protobuf messages from upstream) and updates local settings
342/// and sends notifications downstream.
343/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
344/// upstream.
345impl SettingsObserver {
346 pub fn init(client: &AnyProtoClient) {
347 client.add_entity_message_handler(Self::handle_update_worktree_settings);
348 }
349
350 pub fn new_local(
351 fs: Arc<dyn Fs>,
352 worktree_store: Entity<WorktreeStore>,
353 task_store: Entity<TaskStore>,
354 cx: &mut Context<Self>,
355 ) -> Self {
356 cx.subscribe(&worktree_store, Self::on_worktree_store_event)
357 .detach();
358
359 Self {
360 worktree_store,
361 task_store,
362 mode: SettingsObserverMode::Local(fs.clone()),
363 downstream_client: None,
364 project_id: 0,
365 _global_task_config_watchers: (
366 Self::subscribe_to_global_task_file_changes(
367 fs.clone(),
368 TaskKind::Script,
369 paths::tasks_file().clone(),
370 cx,
371 ),
372 Self::subscribe_to_global_task_file_changes(
373 fs,
374 TaskKind::Debug,
375 paths::debug_tasks_file().clone(),
376 cx,
377 ),
378 ),
379 }
380 }
381
382 pub fn new_remote(
383 fs: Arc<dyn Fs>,
384 worktree_store: Entity<WorktreeStore>,
385 task_store: Entity<TaskStore>,
386 cx: &mut Context<Self>,
387 ) -> Self {
388 Self {
389 worktree_store,
390 task_store,
391 mode: SettingsObserverMode::Remote,
392 downstream_client: None,
393 project_id: 0,
394 _global_task_config_watchers: (
395 Self::subscribe_to_global_task_file_changes(
396 fs.clone(),
397 TaskKind::Script,
398 paths::tasks_file().clone(),
399 cx,
400 ),
401 Self::subscribe_to_global_task_file_changes(
402 fs.clone(),
403 TaskKind::Debug,
404 paths::debug_tasks_file().clone(),
405 cx,
406 ),
407 ),
408 }
409 }
410
411 pub fn shared(
412 &mut self,
413 project_id: u64,
414 downstream_client: AnyProtoClient,
415 cx: &mut Context<Self>,
416 ) {
417 self.project_id = project_id;
418 self.downstream_client = Some(downstream_client.clone());
419
420 let store = cx.global::<SettingsStore>();
421 for worktree in self.worktree_store.read(cx).worktrees() {
422 let worktree_id = worktree.read(cx).id().to_proto();
423 for (path, content) in store.local_settings(worktree.read(cx).id()) {
424 downstream_client
425 .send(proto::UpdateWorktreeSettings {
426 project_id,
427 worktree_id,
428 path: path.to_proto(),
429 content: Some(content),
430 kind: Some(
431 local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
432 ),
433 })
434 .log_err();
435 }
436 for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
437 downstream_client
438 .send(proto::UpdateWorktreeSettings {
439 project_id,
440 worktree_id,
441 path: path.to_proto(),
442 content: Some(content),
443 kind: Some(
444 local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
445 ),
446 })
447 .log_err();
448 }
449 }
450 }
451
452 pub fn unshared(&mut self, _: &mut Context<Self>) {
453 self.downstream_client = None;
454 }
455
456 async fn handle_update_worktree_settings(
457 this: Entity<Self>,
458 envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
459 mut cx: AsyncApp,
460 ) -> anyhow::Result<()> {
461 let kind = match envelope.payload.kind {
462 Some(kind) => proto::LocalSettingsKind::from_i32(kind)
463 .with_context(|| format!("unknown kind {kind}"))?,
464 None => proto::LocalSettingsKind::Settings,
465 };
466 this.update(&mut cx, |this, cx| {
467 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
468 let Some(worktree) = this
469 .worktree_store
470 .read(cx)
471 .worktree_for_id(worktree_id, cx)
472 else {
473 return;
474 };
475
476 this.update_settings(
477 worktree,
478 [(
479 Arc::<Path>::from_proto(envelope.payload.path.clone()),
480 local_settings_kind_from_proto(kind),
481 envelope.payload.content,
482 )],
483 cx,
484 );
485 })?;
486 Ok(())
487 }
488
489 fn on_worktree_store_event(
490 &mut self,
491 _: Entity<WorktreeStore>,
492 event: &WorktreeStoreEvent,
493 cx: &mut Context<Self>,
494 ) {
495 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
496 cx.subscribe(worktree, |this, worktree, event, cx| {
497 if let worktree::Event::UpdatedEntries(changes) = event {
498 this.update_local_worktree_settings(&worktree, changes, cx)
499 }
500 })
501 .detach()
502 }
503 }
504
505 fn update_local_worktree_settings(
506 &mut self,
507 worktree: &Entity<Worktree>,
508 changes: &UpdatedEntriesSet,
509 cx: &mut Context<Self>,
510 ) {
511 let SettingsObserverMode::Local(fs) = &self.mode else {
512 return;
513 };
514
515 let mut settings_contents = Vec::new();
516 for (path, _, change) in changes.iter() {
517 let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
518 let settings_dir = Arc::<Path>::from(
519 path.ancestors()
520 .nth(local_settings_file_relative_path().components().count())
521 .unwrap(),
522 );
523 (settings_dir, LocalSettingsKind::Settings)
524 } else if path.ends_with(local_tasks_file_relative_path()) {
525 let settings_dir = Arc::<Path>::from(
526 path.ancestors()
527 .nth(
528 local_tasks_file_relative_path()
529 .components()
530 .count()
531 .saturating_sub(1),
532 )
533 .unwrap(),
534 );
535 (settings_dir, LocalSettingsKind::Tasks(TaskKind::Script))
536 } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
537 let settings_dir = Arc::<Path>::from(
538 path.ancestors()
539 .nth(
540 local_vscode_tasks_file_relative_path()
541 .components()
542 .count()
543 .saturating_sub(1),
544 )
545 .unwrap(),
546 );
547 (settings_dir, LocalSettingsKind::Tasks(TaskKind::Script))
548 } else if path.ends_with(local_debug_file_relative_path()) {
549 let settings_dir = Arc::<Path>::from(
550 path.ancestors()
551 .nth(
552 local_debug_file_relative_path()
553 .components()
554 .count()
555 .saturating_sub(1),
556 )
557 .unwrap(),
558 );
559 (settings_dir, LocalSettingsKind::Tasks(TaskKind::Debug))
560 } else if path.ends_with(EDITORCONFIG_NAME) {
561 let Some(settings_dir) = path.parent().map(Arc::from) else {
562 continue;
563 };
564 (settings_dir, LocalSettingsKind::Editorconfig)
565 } else {
566 continue;
567 };
568
569 let removed = change == &PathChange::Removed;
570 let fs = fs.clone();
571 let abs_path = match worktree.read(cx).absolutize(path) {
572 Ok(abs_path) => abs_path,
573 Err(e) => {
574 log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
575 continue;
576 }
577 };
578 settings_contents.push(async move {
579 (
580 settings_dir,
581 kind,
582 if removed {
583 None
584 } else {
585 Some(
586 async move {
587 let content = fs.load(&abs_path).await?;
588 if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
589 let vscode_tasks =
590 parse_json_with_comments::<VsCodeTaskFile>(&content)
591 .with_context(|| {
592 format!("parsing VSCode tasks, file {abs_path:?}")
593 })?;
594 let zed_tasks = TaskTemplates::try_from(vscode_tasks)
595 .with_context(|| {
596 format!(
597 "converting VSCode tasks into Zed ones, file {abs_path:?}"
598 )
599 })?;
600 serde_json::to_string(&zed_tasks).with_context(|| {
601 format!(
602 "serializing Zed tasks into JSON, file {abs_path:?}"
603 )
604 })
605 } else {
606 Ok(content)
607 }
608 }
609 .await,
610 )
611 },
612 )
613 });
614 }
615
616 if settings_contents.is_empty() {
617 return;
618 }
619
620 let worktree = worktree.clone();
621 cx.spawn(async move |this, cx| {
622 let settings_contents: Vec<(Arc<Path>, _, _)> =
623 futures::future::join_all(settings_contents).await;
624 cx.update(|cx| {
625 this.update(cx, |this, cx| {
626 this.update_settings(
627 worktree,
628 settings_contents.into_iter().map(|(path, kind, content)| {
629 (path, kind, content.and_then(|c| c.log_err()))
630 }),
631 cx,
632 )
633 })
634 })
635 })
636 .detach();
637 }
638
639 fn update_settings(
640 &mut self,
641 worktree: Entity<Worktree>,
642 settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
643 cx: &mut Context<Self>,
644 ) {
645 let worktree_id = worktree.read(cx).id();
646 let remote_worktree_id = worktree.read(cx).id();
647 let task_store = self.task_store.clone();
648
649 for (directory, kind, file_content) in settings_contents {
650 match kind {
651 LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
652 .update_global::<SettingsStore, _>(|store, cx| {
653 let result = store.set_local_settings(
654 worktree_id,
655 directory.clone(),
656 kind,
657 file_content.as_deref(),
658 cx,
659 );
660
661 match result {
662 Err(InvalidSettingsError::LocalSettings { path, message }) => {
663 log::error!("Failed to set local settings in {path:?}: {message}");
664 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
665 InvalidSettingsError::LocalSettings { path, message },
666 )));
667 }
668 Err(e) => {
669 log::error!("Failed to set local settings: {e}");
670 }
671 Ok(()) => {
672 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(
673 directory.join(local_settings_file_relative_path())
674 )));
675 }
676 }
677 }),
678 LocalSettingsKind::Tasks(task_kind) => {
679 let result = task_store.update(cx, |task_store, cx| {
680 task_store.update_user_tasks(
681 TaskSettingsLocation::Worktree(SettingsLocation {
682 worktree_id,
683 path: directory.as_ref(),
684 }),
685 file_content.as_deref(),
686 task_kind,
687 cx,
688 )
689 });
690
691 match result {
692 Err(InvalidSettingsError::Tasks { path, message }) => {
693 log::error!("Failed to set local tasks in {path:?}: {message:?}");
694 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
695 InvalidSettingsError::Tasks { path, message },
696 )));
697 }
698 Err(e) => {
699 log::error!("Failed to set local tasks: {e}");
700 }
701 Ok(()) => {
702 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
703 task_kind.config_in_dir(&directory)
704 )));
705 }
706 }
707 }
708 };
709
710 if let Some(downstream_client) = &self.downstream_client {
711 downstream_client
712 .send(proto::UpdateWorktreeSettings {
713 project_id: self.project_id,
714 worktree_id: remote_worktree_id.to_proto(),
715 path: directory.to_proto(),
716 content: file_content,
717 kind: Some(local_settings_kind_to_proto(kind).into()),
718 })
719 .log_err();
720 }
721 }
722 }
723
724 fn subscribe_to_global_task_file_changes(
725 fs: Arc<dyn Fs>,
726 task_kind: TaskKind,
727 file_path: PathBuf,
728 cx: &mut Context<Self>,
729 ) -> Task<()> {
730 let mut user_tasks_file_rx =
731 watch_config_file(&cx.background_executor(), fs, file_path.clone());
732 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
733 let weak_entry = cx.weak_entity();
734 cx.spawn(async move |settings_observer, cx| {
735 let Ok(task_store) = settings_observer.update(cx, |settings_observer, _| {
736 settings_observer.task_store.clone()
737 }) else {
738 return;
739 };
740 if let Some(user_tasks_content) = user_tasks_content {
741 let Ok(()) = task_store.update(cx, |task_store, cx| {
742 task_store
743 .update_user_tasks(
744 TaskSettingsLocation::Global(&file_path),
745 Some(&user_tasks_content),
746 task_kind,
747 cx,
748 )
749 .log_err();
750 }) else {
751 return;
752 };
753 }
754 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
755 let Ok(result) = task_store.update(cx, |task_store, cx| {
756 task_store.update_user_tasks(
757 TaskSettingsLocation::Global(&file_path),
758 Some(&user_tasks_content),
759 task_kind,
760 cx,
761 )
762 }) else {
763 break;
764 };
765
766 weak_entry
767 .update(cx, |_, cx| match result {
768 Ok(()) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
769 file_path.clone()
770 ))),
771 Err(err) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
772 InvalidSettingsError::Tasks {
773 path: file_path.clone(),
774 message: err.to_string(),
775 },
776 ))),
777 })
778 .ok();
779 }
780 })
781 }
782}
783
784pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
785 match kind {
786 proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
787 proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks(TaskKind::Script),
788 proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
789 }
790}
791
792pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
793 match kind {
794 LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
795 LocalSettingsKind::Tasks(_) => proto::LocalSettingsKind::Tasks,
796 LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
797 }
798}