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