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