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