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