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 collections::BTreeMap,
26 path::{Path, PathBuf},
27 sync::Arc,
28 time::Duration,
29};
30use task::{DebugTaskFile, TaskTemplates, VsCodeDebugTaskFile, VsCodeTaskFile};
31use util::{ResultExt, serde::default_true};
32use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
33
34use crate::{
35 task_store::{TaskSettingsLocation, TaskStore},
36 worktree_store::{WorktreeStore, WorktreeStoreEvent},
37};
38
39#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
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
192 .as_ref()
193 .is_some_and(|cargo_diagnostics| cargo_diagnostics.fetch_cargo_diagnostics)
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/// Determines the severity of the diagnostic that should be moved to.
331#[derive(PartialEq, PartialOrd, Clone, Copy, Debug, Eq, Deserialize, JsonSchema)]
332#[serde(rename_all = "snake_case")]
333pub enum GoToDiagnosticSeverity {
334 /// Errors
335 Error = 3,
336 /// Warnings
337 Warning = 2,
338 /// Information
339 Information = 1,
340 /// Hints
341 Hint = 0,
342}
343
344impl From<lsp::DiagnosticSeverity> for GoToDiagnosticSeverity {
345 fn from(severity: lsp::DiagnosticSeverity) -> Self {
346 match severity {
347 lsp::DiagnosticSeverity::ERROR => Self::Error,
348 lsp::DiagnosticSeverity::WARNING => Self::Warning,
349 lsp::DiagnosticSeverity::INFORMATION => Self::Information,
350 lsp::DiagnosticSeverity::HINT => Self::Hint,
351 _ => Self::Error,
352 }
353 }
354}
355
356impl GoToDiagnosticSeverity {
357 pub fn min() -> Self {
358 Self::Hint
359 }
360
361 pub fn max() -> Self {
362 Self::Error
363 }
364}
365
366/// Allows filtering diagnostics that should be moved to.
367#[derive(PartialEq, Clone, Copy, Debug, Deserialize, JsonSchema)]
368#[serde(untagged)]
369pub enum GoToDiagnosticSeverityFilter {
370 /// Move to diagnostics of a specific severity.
371 Only(GoToDiagnosticSeverity),
372
373 /// Specify a range of severities to include.
374 Range {
375 /// Minimum severity to move to. Defaults no "error".
376 #[serde(default = "GoToDiagnosticSeverity::min")]
377 min: GoToDiagnosticSeverity,
378 /// Maximum severity to move to. Defaults to "hint".
379 #[serde(default = "GoToDiagnosticSeverity::max")]
380 max: GoToDiagnosticSeverity,
381 },
382}
383
384impl Default for GoToDiagnosticSeverityFilter {
385 fn default() -> Self {
386 Self::Range {
387 min: GoToDiagnosticSeverity::min(),
388 max: GoToDiagnosticSeverity::max(),
389 }
390 }
391}
392
393impl GoToDiagnosticSeverityFilter {
394 pub fn matches(&self, severity: lsp::DiagnosticSeverity) -> bool {
395 let severity: GoToDiagnosticSeverity = severity.into();
396 match self {
397 Self::Only(target) => *target == severity,
398 Self::Range { min, max } => severity >= *min && severity <= *max,
399 }
400 }
401}
402
403#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
404pub struct GitSettings {
405 /// Whether or not to show the git gutter.
406 ///
407 /// Default: tracked_files
408 pub git_gutter: Option<GitGutterSetting>,
409 /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
410 ///
411 /// Default: null
412 pub gutter_debounce: Option<u64>,
413 /// Whether or not to show git blame data inline in
414 /// the currently focused line.
415 ///
416 /// Default: on
417 pub inline_blame: Option<InlineBlameSettings>,
418 /// How hunks are displayed visually in the editor.
419 ///
420 /// Default: staged_hollow
421 pub hunk_style: Option<GitHunkStyleSetting>,
422}
423
424impl GitSettings {
425 pub fn inline_blame_enabled(&self) -> bool {
426 #[allow(unknown_lints, clippy::manual_unwrap_or_default)]
427 match self.inline_blame {
428 Some(InlineBlameSettings { enabled, .. }) => enabled,
429 _ => false,
430 }
431 }
432
433 pub fn inline_blame_delay(&self) -> Option<Duration> {
434 match self.inline_blame {
435 Some(InlineBlameSettings { delay_ms, .. }) if delay_ms > 0 => {
436 Some(Duration::from_millis(delay_ms))
437 }
438 _ => None,
439 }
440 }
441
442 pub fn show_inline_commit_summary(&self) -> bool {
443 match self.inline_blame {
444 Some(InlineBlameSettings {
445 show_commit_summary,
446 ..
447 }) => show_commit_summary,
448 _ => false,
449 }
450 }
451}
452
453#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
454#[serde(rename_all = "snake_case")]
455pub enum GitHunkStyleSetting {
456 /// Show unstaged hunks with a filled background and staged hunks hollow.
457 #[default]
458 StagedHollow,
459 /// Show unstaged hunks hollow and staged hunks with a filled background.
460 UnstagedHollow,
461}
462
463#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
464#[serde(rename_all = "snake_case")]
465pub enum GitGutterSetting {
466 /// Show git gutter in tracked files.
467 #[default]
468 TrackedFiles,
469 /// Hide git gutter
470 Hide,
471}
472
473#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
474#[serde(rename_all = "snake_case")]
475pub struct InlineBlameSettings {
476 /// Whether or not to show git blame data inline in
477 /// the currently focused line.
478 ///
479 /// Default: true
480 #[serde(default = "default_true")]
481 pub enabled: bool,
482 /// Whether to only show the inline blame information
483 /// after a delay once the cursor stops moving.
484 ///
485 /// Default: 0
486 #[serde(default)]
487 pub delay_ms: u64,
488 /// The amount of padding between the end of the source line and the start
489 /// of the inline blame in units of columns.
490 ///
491 /// Default: 7
492 #[serde(default = "default_inline_blame_padding")]
493 pub padding: u32,
494 /// The minimum column number to show the inline blame information at
495 ///
496 /// Default: 0
497 #[serde(default)]
498 pub min_column: u32,
499 /// Whether to show commit summary as part of the inline blame.
500 ///
501 /// Default: false
502 #[serde(default)]
503 pub show_commit_summary: bool,
504}
505
506fn default_inline_blame_padding() -> u32 {
507 7
508}
509
510impl Default for InlineBlameSettings {
511 fn default() -> Self {
512 Self {
513 enabled: true,
514 delay_ms: 0,
515 padding: default_inline_blame_padding(),
516 min_column: 0,
517 show_commit_summary: false,
518 }
519 }
520}
521
522#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
523pub struct BinarySettings {
524 pub path: Option<String>,
525 pub arguments: Option<Vec<String>>,
526 pub env: Option<BTreeMap<String, String>>,
527 pub ignore_system_version: Option<bool>,
528}
529
530#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
531#[serde(rename_all = "snake_case")]
532pub struct LspSettings {
533 pub binary: Option<BinarySettings>,
534 pub initialization_options: Option<serde_json::Value>,
535 pub settings: Option<serde_json::Value>,
536 /// If the server supports sending tasks over LSP extensions,
537 /// this setting can be used to enable or disable them in Zed.
538 /// Default: true
539 #[serde(default = "default_true")]
540 pub enable_lsp_tasks: bool,
541}
542
543impl Default for LspSettings {
544 fn default() -> Self {
545 Self {
546 binary: None,
547 initialization_options: None,
548 settings: None,
549 enable_lsp_tasks: true,
550 }
551 }
552}
553
554#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
555pub struct SessionSettings {
556 /// Whether or not to restore unsaved buffers on restart.
557 ///
558 /// If this is true, user won't be prompted whether to save/discard
559 /// dirty files when closing the application.
560 ///
561 /// Default: true
562 pub restore_unsaved_buffers: bool,
563}
564
565impl Default for SessionSettings {
566 fn default() -> Self {
567 Self {
568 restore_unsaved_buffers: true,
569 }
570 }
571}
572
573impl Settings for ProjectSettings {
574 const KEY: Option<&'static str> = None;
575
576 type FileContent = Self;
577
578 fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
579 sources.json_merge()
580 }
581
582 fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
583 // this just sets the binary name instead of a full path so it relies on path lookup
584 // resolving to the one you want
585 vscode.enum_setting(
586 "npm.packageManager",
587 &mut current.node.npm_path,
588 |s| match s {
589 v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
590 _ => None,
591 },
592 );
593
594 if let Some(b) = vscode.read_bool("git.blame.editorDecoration.enabled") {
595 if let Some(blame) = current.git.inline_blame.as_mut() {
596 blame.enabled = b
597 } else {
598 current.git.inline_blame = Some(InlineBlameSettings {
599 enabled: b,
600 ..Default::default()
601 })
602 }
603 }
604
605 #[derive(Deserialize)]
606 struct VsCodeContextServerCommand {
607 command: PathBuf,
608 args: Option<Vec<String>>,
609 env: Option<HashMap<String, String>>,
610 // note: we don't support envFile and type
611 }
612 impl From<VsCodeContextServerCommand> for ContextServerCommand {
613 fn from(cmd: VsCodeContextServerCommand) -> Self {
614 Self {
615 path: cmd.command,
616 args: cmd.args.unwrap_or_default(),
617 env: cmd.env,
618 }
619 }
620 }
621 if let Some(mcp) = vscode.read_value("mcp").and_then(|v| v.as_object()) {
622 current
623 .context_servers
624 .extend(mcp.iter().filter_map(|(k, v)| {
625 Some((
626 k.clone().into(),
627 ContextServerSettings::Custom {
628 enabled: true,
629 command: serde_json::from_value::<VsCodeContextServerCommand>(
630 v.clone(),
631 )
632 .ok()?
633 .into(),
634 },
635 ))
636 }));
637 }
638
639 // TODO: translate lsp settings for rust-analyzer and other popular ones to old.lsp
640 }
641}
642
643pub enum SettingsObserverMode {
644 Local(Arc<dyn Fs>),
645 Remote,
646}
647
648#[derive(Clone, Debug, PartialEq)]
649pub enum SettingsObserverEvent {
650 LocalSettingsUpdated(Result<PathBuf, InvalidSettingsError>),
651 LocalTasksUpdated(Result<PathBuf, InvalidSettingsError>),
652 LocalDebugScenariosUpdated(Result<PathBuf, InvalidSettingsError>),
653}
654
655impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
656
657pub struct SettingsObserver {
658 mode: SettingsObserverMode,
659 downstream_client: Option<AnyProtoClient>,
660 worktree_store: Entity<WorktreeStore>,
661 project_id: u64,
662 task_store: Entity<TaskStore>,
663 _global_task_config_watcher: Task<()>,
664 _global_debug_config_watcher: Task<()>,
665}
666
667/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
668/// (or the equivalent protobuf messages from upstream) and updates local settings
669/// and sends notifications downstream.
670/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
671/// upstream.
672impl SettingsObserver {
673 pub fn init(client: &AnyProtoClient) {
674 client.add_entity_message_handler(Self::handle_update_worktree_settings);
675 }
676
677 pub fn new_local(
678 fs: Arc<dyn Fs>,
679 worktree_store: Entity<WorktreeStore>,
680 task_store: Entity<TaskStore>,
681 cx: &mut Context<Self>,
682 ) -> Self {
683 cx.subscribe(&worktree_store, Self::on_worktree_store_event)
684 .detach();
685
686 Self {
687 worktree_store,
688 task_store,
689 mode: SettingsObserverMode::Local(fs.clone()),
690 downstream_client: None,
691 project_id: 0,
692 _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
693 fs.clone(),
694 paths::tasks_file().clone(),
695 cx,
696 ),
697 _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
698 fs.clone(),
699 paths::debug_scenarios_file().clone(),
700 cx,
701 ),
702 }
703 }
704
705 pub fn new_remote(
706 fs: Arc<dyn Fs>,
707 worktree_store: Entity<WorktreeStore>,
708 task_store: Entity<TaskStore>,
709 cx: &mut Context<Self>,
710 ) -> Self {
711 Self {
712 worktree_store,
713 task_store,
714 mode: SettingsObserverMode::Remote,
715 downstream_client: None,
716 project_id: 0,
717 _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
718 fs.clone(),
719 paths::tasks_file().clone(),
720 cx,
721 ),
722 _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
723 fs.clone(),
724 paths::debug_scenarios_file().clone(),
725 cx,
726 ),
727 }
728 }
729
730 pub fn shared(
731 &mut self,
732 project_id: u64,
733 downstream_client: AnyProtoClient,
734 cx: &mut Context<Self>,
735 ) {
736 self.project_id = project_id;
737 self.downstream_client = Some(downstream_client.clone());
738
739 let store = cx.global::<SettingsStore>();
740 for worktree in self.worktree_store.read(cx).worktrees() {
741 let worktree_id = worktree.read(cx).id().to_proto();
742 for (path, content) in store.local_settings(worktree.read(cx).id()) {
743 downstream_client
744 .send(proto::UpdateWorktreeSettings {
745 project_id,
746 worktree_id,
747 path: path.to_proto(),
748 content: Some(content),
749 kind: Some(
750 local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
751 ),
752 })
753 .log_err();
754 }
755 for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
756 downstream_client
757 .send(proto::UpdateWorktreeSettings {
758 project_id,
759 worktree_id,
760 path: path.to_proto(),
761 content: Some(content),
762 kind: Some(
763 local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
764 ),
765 })
766 .log_err();
767 }
768 }
769 }
770
771 pub fn unshared(&mut self, _: &mut Context<Self>) {
772 self.downstream_client = None;
773 }
774
775 async fn handle_update_worktree_settings(
776 this: Entity<Self>,
777 envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
778 mut cx: AsyncApp,
779 ) -> anyhow::Result<()> {
780 let kind = match envelope.payload.kind {
781 Some(kind) => proto::LocalSettingsKind::from_i32(kind)
782 .with_context(|| format!("unknown kind {kind}"))?,
783 None => proto::LocalSettingsKind::Settings,
784 };
785 this.update(&mut cx, |this, cx| {
786 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
787 let Some(worktree) = this
788 .worktree_store
789 .read(cx)
790 .worktree_for_id(worktree_id, cx)
791 else {
792 return;
793 };
794
795 this.update_settings(
796 worktree,
797 [(
798 Arc::<Path>::from_proto(envelope.payload.path.clone()),
799 local_settings_kind_from_proto(kind),
800 envelope.payload.content,
801 )],
802 cx,
803 );
804 })?;
805 Ok(())
806 }
807
808 fn on_worktree_store_event(
809 &mut self,
810 _: Entity<WorktreeStore>,
811 event: &WorktreeStoreEvent,
812 cx: &mut Context<Self>,
813 ) {
814 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
815 cx.subscribe(worktree, |this, worktree, event, cx| {
816 if let worktree::Event::UpdatedEntries(changes) = event {
817 this.update_local_worktree_settings(&worktree, changes, cx)
818 }
819 })
820 .detach()
821 }
822 }
823
824 fn update_local_worktree_settings(
825 &mut self,
826 worktree: &Entity<Worktree>,
827 changes: &UpdatedEntriesSet,
828 cx: &mut Context<Self>,
829 ) {
830 let SettingsObserverMode::Local(fs) = &self.mode else {
831 return;
832 };
833
834 let mut settings_contents = Vec::new();
835 for (path, _, change) in changes.iter() {
836 let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
837 let settings_dir = Arc::<Path>::from(
838 path.ancestors()
839 .nth(local_settings_file_relative_path().components().count())
840 .unwrap(),
841 );
842 (settings_dir, LocalSettingsKind::Settings)
843 } else if path.ends_with(local_tasks_file_relative_path()) {
844 let settings_dir = Arc::<Path>::from(
845 path.ancestors()
846 .nth(
847 local_tasks_file_relative_path()
848 .components()
849 .count()
850 .saturating_sub(1),
851 )
852 .unwrap(),
853 );
854 (settings_dir, LocalSettingsKind::Tasks)
855 } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
856 let settings_dir = Arc::<Path>::from(
857 path.ancestors()
858 .nth(
859 local_vscode_tasks_file_relative_path()
860 .components()
861 .count()
862 .saturating_sub(1),
863 )
864 .unwrap(),
865 );
866 (settings_dir, LocalSettingsKind::Tasks)
867 } else if path.ends_with(local_debug_file_relative_path()) {
868 let settings_dir = Arc::<Path>::from(
869 path.ancestors()
870 .nth(
871 local_debug_file_relative_path()
872 .components()
873 .count()
874 .saturating_sub(1),
875 )
876 .unwrap(),
877 );
878 (settings_dir, LocalSettingsKind::Debug)
879 } else if path.ends_with(local_vscode_launch_file_relative_path()) {
880 let settings_dir = Arc::<Path>::from(
881 path.ancestors()
882 .nth(
883 local_vscode_tasks_file_relative_path()
884 .components()
885 .count()
886 .saturating_sub(1),
887 )
888 .unwrap(),
889 );
890 (settings_dir, LocalSettingsKind::Debug)
891 } else if path.ends_with(EDITORCONFIG_NAME) {
892 let Some(settings_dir) = path.parent().map(Arc::from) else {
893 continue;
894 };
895 (settings_dir, LocalSettingsKind::Editorconfig)
896 } else {
897 continue;
898 };
899
900 let removed = change == &PathChange::Removed;
901 let fs = fs.clone();
902 let abs_path = match worktree.read(cx).absolutize(path) {
903 Ok(abs_path) => abs_path,
904 Err(e) => {
905 log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
906 continue;
907 }
908 };
909 settings_contents.push(async move {
910 (
911 settings_dir,
912 kind,
913 if removed {
914 None
915 } else {
916 Some(
917 async move {
918 let content = fs.load(&abs_path).await?;
919 if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
920 let vscode_tasks =
921 parse_json_with_comments::<VsCodeTaskFile>(&content)
922 .with_context(|| {
923 format!("parsing VSCode tasks, file {abs_path:?}")
924 })?;
925 let zed_tasks = TaskTemplates::try_from(vscode_tasks)
926 .with_context(|| {
927 format!(
928 "converting VSCode tasks into Zed ones, file {abs_path:?}"
929 )
930 })?;
931 serde_json::to_string(&zed_tasks).with_context(|| {
932 format!(
933 "serializing Zed tasks into JSON, file {abs_path:?}"
934 )
935 })
936 } else if abs_path.ends_with(local_vscode_launch_file_relative_path()) {
937 let vscode_tasks =
938 parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
939 .with_context(|| {
940 format!("parsing VSCode debug tasks, file {abs_path:?}")
941 })?;
942 let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
943 .with_context(|| {
944 format!(
945 "converting VSCode debug tasks into Zed ones, file {abs_path:?}"
946 )
947 })?;
948 serde_json::to_string(&zed_tasks).with_context(|| {
949 format!(
950 "serializing Zed tasks into JSON, file {abs_path:?}"
951 )
952 })
953 } else {
954 Ok(content)
955 }
956 }
957 .await,
958 )
959 },
960 )
961 });
962 }
963
964 if settings_contents.is_empty() {
965 return;
966 }
967
968 let worktree = worktree.clone();
969 cx.spawn(async move |this, cx| {
970 let settings_contents: Vec<(Arc<Path>, _, _)> =
971 futures::future::join_all(settings_contents).await;
972 cx.update(|cx| {
973 this.update(cx, |this, cx| {
974 this.update_settings(
975 worktree,
976 settings_contents.into_iter().map(|(path, kind, content)| {
977 (path, kind, content.and_then(|c| c.log_err()))
978 }),
979 cx,
980 )
981 })
982 })
983 })
984 .detach();
985 }
986
987 fn update_settings(
988 &mut self,
989 worktree: Entity<Worktree>,
990 settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
991 cx: &mut Context<Self>,
992 ) {
993 let worktree_id = worktree.read(cx).id();
994 let remote_worktree_id = worktree.read(cx).id();
995 let task_store = self.task_store.clone();
996
997 for (directory, kind, file_content) in settings_contents {
998 match kind {
999 LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
1000 .update_global::<SettingsStore, _>(|store, cx| {
1001 let result = store.set_local_settings(
1002 worktree_id,
1003 directory.clone(),
1004 kind,
1005 file_content.as_deref(),
1006 cx,
1007 );
1008
1009 match result {
1010 Err(InvalidSettingsError::LocalSettings { path, message }) => {
1011 log::error!("Failed to set local settings in {path:?}: {message}");
1012 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
1013 InvalidSettingsError::LocalSettings { path, message },
1014 )));
1015 }
1016 Err(e) => {
1017 log::error!("Failed to set local settings: {e}");
1018 }
1019 Ok(()) => {
1020 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(
1021 directory.join(local_settings_file_relative_path())
1022 )));
1023 }
1024 }
1025 }),
1026 LocalSettingsKind::Tasks => {
1027 let result = task_store.update(cx, |task_store, cx| {
1028 task_store.update_user_tasks(
1029 TaskSettingsLocation::Worktree(SettingsLocation {
1030 worktree_id,
1031 path: directory.as_ref(),
1032 }),
1033 file_content.as_deref(),
1034 cx,
1035 )
1036 });
1037
1038 match result {
1039 Err(InvalidSettingsError::Tasks { path, message }) => {
1040 log::error!("Failed to set local tasks in {path:?}: {message:?}");
1041 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1042 InvalidSettingsError::Tasks { path, message },
1043 )));
1044 }
1045 Err(e) => {
1046 log::error!("Failed to set local tasks: {e}");
1047 }
1048 Ok(()) => {
1049 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1050 directory.join(task_file_name())
1051 )));
1052 }
1053 }
1054 }
1055 LocalSettingsKind::Debug => {
1056 let result = task_store.update(cx, |task_store, cx| {
1057 task_store.update_user_debug_scenarios(
1058 TaskSettingsLocation::Worktree(SettingsLocation {
1059 worktree_id,
1060 path: directory.as_ref(),
1061 }),
1062 file_content.as_deref(),
1063 cx,
1064 )
1065 });
1066
1067 match result {
1068 Err(InvalidSettingsError::Debug { path, message }) => {
1069 log::error!(
1070 "Failed to set local debug scenarios in {path:?}: {message:?}"
1071 );
1072 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1073 InvalidSettingsError::Debug { path, message },
1074 )));
1075 }
1076 Err(e) => {
1077 log::error!("Failed to set local tasks: {e}");
1078 }
1079 Ok(()) => {
1080 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1081 directory.join(task_file_name())
1082 )));
1083 }
1084 }
1085 }
1086 };
1087
1088 if let Some(downstream_client) = &self.downstream_client {
1089 downstream_client
1090 .send(proto::UpdateWorktreeSettings {
1091 project_id: self.project_id,
1092 worktree_id: remote_worktree_id.to_proto(),
1093 path: directory.to_proto(),
1094 content: file_content,
1095 kind: Some(local_settings_kind_to_proto(kind).into()),
1096 })
1097 .log_err();
1098 }
1099 }
1100 }
1101
1102 fn subscribe_to_global_task_file_changes(
1103 fs: Arc<dyn Fs>,
1104 file_path: PathBuf,
1105 cx: &mut Context<Self>,
1106 ) -> Task<()> {
1107 let mut user_tasks_file_rx =
1108 watch_config_file(cx.background_executor(), fs, file_path.clone());
1109 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1110 let weak_entry = cx.weak_entity();
1111 cx.spawn(async move |settings_observer, cx| {
1112 let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1113 settings_observer.task_store.clone()
1114 }) else {
1115 return;
1116 };
1117 if let Some(user_tasks_content) = user_tasks_content {
1118 let Ok(()) = task_store.update(cx, |task_store, cx| {
1119 task_store
1120 .update_user_tasks(
1121 TaskSettingsLocation::Global(&file_path),
1122 Some(&user_tasks_content),
1123 cx,
1124 )
1125 .log_err();
1126 }) else {
1127 return;
1128 };
1129 }
1130 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1131 let Ok(result) = task_store.update(cx, |task_store, cx| {
1132 task_store.update_user_tasks(
1133 TaskSettingsLocation::Global(&file_path),
1134 Some(&user_tasks_content),
1135 cx,
1136 )
1137 }) else {
1138 break;
1139 };
1140
1141 weak_entry
1142 .update(cx, |_, cx| match result {
1143 Ok(()) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1144 file_path.clone()
1145 ))),
1146 Err(err) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1147 InvalidSettingsError::Tasks {
1148 path: file_path.clone(),
1149 message: err.to_string(),
1150 },
1151 ))),
1152 })
1153 .ok();
1154 }
1155 })
1156 }
1157 fn subscribe_to_global_debug_scenarios_changes(
1158 fs: Arc<dyn Fs>,
1159 file_path: PathBuf,
1160 cx: &mut Context<Self>,
1161 ) -> Task<()> {
1162 let mut user_tasks_file_rx =
1163 watch_config_file(cx.background_executor(), fs, file_path.clone());
1164 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1165 let weak_entry = cx.weak_entity();
1166 cx.spawn(async move |settings_observer, cx| {
1167 let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1168 settings_observer.task_store.clone()
1169 }) else {
1170 return;
1171 };
1172 if let Some(user_tasks_content) = user_tasks_content {
1173 let Ok(()) = task_store.update(cx, |task_store, cx| {
1174 task_store
1175 .update_user_debug_scenarios(
1176 TaskSettingsLocation::Global(&file_path),
1177 Some(&user_tasks_content),
1178 cx,
1179 )
1180 .log_err();
1181 }) else {
1182 return;
1183 };
1184 }
1185 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1186 let Ok(result) = task_store.update(cx, |task_store, cx| {
1187 task_store.update_user_debug_scenarios(
1188 TaskSettingsLocation::Global(&file_path),
1189 Some(&user_tasks_content),
1190 cx,
1191 )
1192 }) else {
1193 break;
1194 };
1195
1196 weak_entry
1197 .update(cx, |_, cx| match result {
1198 Ok(()) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(Ok(
1199 file_path.clone(),
1200 ))),
1201 Err(err) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(
1202 Err(InvalidSettingsError::Tasks {
1203 path: file_path.clone(),
1204 message: err.to_string(),
1205 }),
1206 )),
1207 })
1208 .ok();
1209 }
1210 })
1211 }
1212}
1213
1214pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
1215 match kind {
1216 proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
1217 proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
1218 proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
1219 proto::LocalSettingsKind::Debug => LocalSettingsKind::Debug,
1220 }
1221}
1222
1223pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
1224 match kind {
1225 LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
1226 LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
1227 LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
1228 LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug,
1229 }
1230}