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