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