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