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