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