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)]
39pub struct ProjectSettings {
40 /// Configuration for language servers.
41 ///
42 /// The following settings can be overridden for specific language servers:
43 /// - initialization_options
44 ///
45 /// To override settings for a language, add an entry for that language server's
46 /// name to the lsp value.
47 /// Default: null
48 #[serde(default)]
49 pub lsp: HashMap<LanguageServerName, LspSettings>,
50
51 /// Common language server settings.
52 #[serde(default)]
53 pub global_lsp_settings: GlobalLspSettings,
54
55 /// Configuration for Debugger-related features
56 #[serde(default)]
57 pub dap: HashMap<DebugAdapterName, DapSettings>,
58
59 /// Settings for context servers used for AI-related features.
60 #[serde(default)]
61 pub context_servers: HashMap<Arc<str>, ContextServerSettings>,
62
63 /// Configuration for Diagnostics-related features.
64 #[serde(default)]
65 pub diagnostics: DiagnosticsSettings,
66
67 /// Configuration for Git-related features
68 #[serde(default)]
69 pub git: GitSettings,
70
71 /// Configuration for Node-related features
72 #[serde(default)]
73 pub node: NodeBinarySettings,
74
75 /// Configuration for how direnv configuration should be loaded
76 #[serde(default)]
77 pub load_direnv: DirenvSettings,
78
79 /// Configuration for session-related features
80 #[serde(default)]
81 pub session: SessionSettings,
82}
83
84#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
85#[serde(rename_all = "snake_case")]
86pub struct DapSettings {
87 pub binary: Option<String>,
88 #[serde(default)]
89 pub args: Vec<String>,
90}
91
92#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
93#[serde(tag = "source", rename_all = "snake_case")]
94pub enum ContextServerSettings {
95 Custom {
96 /// Whether the context server is enabled.
97 #[serde(default = "default_true")]
98 enabled: bool,
99
100 #[serde(flatten)]
101 command: ContextServerCommand,
102 },
103 Extension {
104 /// Whether the context server is enabled.
105 #[serde(default = "default_true")]
106 enabled: bool,
107 /// The settings for this context server specified by the extension.
108 ///
109 /// Consult the documentation for the context server to see what settings
110 /// are supported.
111 settings: serde_json::Value,
112 },
113}
114
115/// Common language server settings.
116#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
117pub struct GlobalLspSettings {
118 /// Whether to show the LSP servers button in the status bar.
119 ///
120 /// Default: `true`
121 #[serde(default = "default_true")]
122 pub button: bool,
123}
124
125impl ContextServerSettings {
126 pub fn default_extension() -> Self {
127 Self::Extension {
128 enabled: true,
129 settings: serde_json::json!({}),
130 }
131 }
132
133 pub fn enabled(&self) -> bool {
134 match self {
135 ContextServerSettings::Custom { enabled, .. } => *enabled,
136 ContextServerSettings::Extension { enabled, .. } => *enabled,
137 }
138 }
139
140 pub fn set_enabled(&mut self, enabled: bool) {
141 match self {
142 ContextServerSettings::Custom { enabled: e, .. } => *e = enabled,
143 ContextServerSettings::Extension { enabled: e, .. } => *e = enabled,
144 }
145 }
146}
147
148#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
149pub struct NodeBinarySettings {
150 /// The path to the Node binary.
151 pub path: Option<String>,
152 /// The path to the npm binary Zed should use (defaults to `.path/../npm`).
153 pub npm_path: Option<String>,
154 /// If enabled, Zed will download its own copy of Node.
155 #[serde(default)]
156 pub ignore_system_version: bool,
157}
158
159#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
160#[serde(rename_all = "snake_case")]
161pub enum DirenvSettings {
162 /// Load direnv configuration through a shell hook
163 ShellHook,
164 /// Load direnv configuration directly using `direnv export json`
165 #[default]
166 Direct,
167}
168
169#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
170#[serde(default)]
171pub struct DiagnosticsSettings {
172 /// Whether to show the project diagnostics button in the status bar.
173 pub button: bool,
174
175 /// Whether or not to include warning diagnostics.
176 pub include_warnings: bool,
177
178 /// Settings for using LSP pull diagnostics mechanism in Zed.
179 pub lsp_pull_diagnostics: LspPullDiagnosticsSettings,
180
181 /// Settings for showing inline diagnostics.
182 pub inline: InlineDiagnosticsSettings,
183
184 /// Configuration, related to Rust language diagnostics.
185 pub cargo: Option<CargoDiagnosticsSettings>,
186}
187
188impl DiagnosticsSettings {
189 pub fn fetch_cargo_diagnostics(&self) -> bool {
190 self.cargo.as_ref().map_or(false, |cargo_diagnostics| {
191 cargo_diagnostics.fetch_cargo_diagnostics
192 })
193 }
194}
195
196#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
197#[serde(default)]
198pub struct LspPullDiagnosticsSettings {
199 /// Whether to pull for diagnostics or not.
200 ///
201 /// Default: true
202 #[serde(default = "default_true")]
203 pub enabled: bool,
204 /// Minimum time to wait before pulling diagnostics from the language server(s).
205 /// 0 turns the debounce off.
206 ///
207 /// Default: 50
208 #[serde(default = "default_lsp_diagnostics_pull_debounce_ms")]
209 pub debounce_ms: u64,
210}
211
212fn default_lsp_diagnostics_pull_debounce_ms() -> u64 {
213 50
214}
215
216#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
217#[serde(default)]
218pub struct InlineDiagnosticsSettings {
219 /// Whether or not to show inline diagnostics
220 ///
221 /// Default: false
222 pub enabled: bool,
223 /// Whether to only show the inline diagnostics after a delay after the
224 /// last editor event.
225 ///
226 /// Default: 150
227 #[serde(default = "default_inline_diagnostics_update_debounce_ms")]
228 pub update_debounce_ms: u64,
229 /// The amount of padding between the end of the source line and the start
230 /// of the inline diagnostic in units of columns.
231 ///
232 /// Default: 4
233 #[serde(default = "default_inline_diagnostics_padding")]
234 pub padding: u32,
235 /// The minimum column to display inline diagnostics. This setting can be
236 /// used to horizontally align inline diagnostics at some position. Lines
237 /// longer than this value will still push diagnostics further to the right.
238 ///
239 /// Default: 0
240 pub min_column: u32,
241
242 pub max_severity: Option<DiagnosticSeverity>,
243}
244
245fn default_inline_diagnostics_update_debounce_ms() -> u64 {
246 150
247}
248
249fn default_inline_diagnostics_padding() -> u32 {
250 4
251}
252
253impl Default for DiagnosticsSettings {
254 fn default() -> Self {
255 Self {
256 button: true,
257 include_warnings: true,
258 lsp_pull_diagnostics: LspPullDiagnosticsSettings::default(),
259 inline: InlineDiagnosticsSettings::default(),
260 cargo: None,
261 }
262 }
263}
264
265impl Default for LspPullDiagnosticsSettings {
266 fn default() -> Self {
267 Self {
268 enabled: true,
269 debounce_ms: default_lsp_diagnostics_pull_debounce_ms(),
270 }
271 }
272}
273
274impl Default for InlineDiagnosticsSettings {
275 fn default() -> Self {
276 Self {
277 enabled: false,
278 update_debounce_ms: default_inline_diagnostics_update_debounce_ms(),
279 padding: default_inline_diagnostics_padding(),
280 min_column: 0,
281 max_severity: None,
282 }
283 }
284}
285
286impl Default for GlobalLspSettings {
287 fn default() -> Self {
288 Self {
289 button: default_true(),
290 }
291 }
292}
293
294#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
295pub struct CargoDiagnosticsSettings {
296 /// When enabled, Zed disables rust-analyzer's check on save and starts to query
297 /// Cargo diagnostics separately.
298 ///
299 /// Default: false
300 #[serde(default)]
301 pub fetch_cargo_diagnostics: bool,
302}
303
304#[derive(
305 Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, JsonSchema,
306)]
307#[serde(rename_all = "snake_case")]
308pub enum DiagnosticSeverity {
309 // No diagnostics are shown.
310 Off,
311 Error,
312 Warning,
313 Info,
314 Hint,
315}
316
317impl DiagnosticSeverity {
318 pub fn into_lsp(self) -> Option<lsp::DiagnosticSeverity> {
319 match self {
320 DiagnosticSeverity::Off => None,
321 DiagnosticSeverity::Error => Some(lsp::DiagnosticSeverity::ERROR),
322 DiagnosticSeverity::Warning => Some(lsp::DiagnosticSeverity::WARNING),
323 DiagnosticSeverity::Info => Some(lsp::DiagnosticSeverity::INFORMATION),
324 DiagnosticSeverity::Hint => Some(lsp::DiagnosticSeverity::HINT),
325 }
326 }
327}
328
329/// Determines the severity of the diagnostic that should be moved to.
330#[derive(PartialEq, PartialOrd, Clone, Copy, Debug, Eq, Deserialize, JsonSchema)]
331#[serde(rename_all = "snake_case")]
332pub enum GoToDiagnosticSeverity {
333 /// Errors
334 Error = 3,
335 /// Warnings
336 Warning = 2,
337 /// Information
338 Information = 1,
339 /// Hints
340 Hint = 0,
341}
342
343impl From<lsp::DiagnosticSeverity> for GoToDiagnosticSeverity {
344 fn from(severity: lsp::DiagnosticSeverity) -> Self {
345 match severity {
346 lsp::DiagnosticSeverity::ERROR => Self::Error,
347 lsp::DiagnosticSeverity::WARNING => Self::Warning,
348 lsp::DiagnosticSeverity::INFORMATION => Self::Information,
349 lsp::DiagnosticSeverity::HINT => Self::Hint,
350 _ => Self::Error,
351 }
352 }
353}
354
355impl GoToDiagnosticSeverity {
356 pub fn min() -> Self {
357 Self::Hint
358 }
359
360 pub fn max() -> Self {
361 Self::Error
362 }
363}
364
365/// Allows filtering diagnostics that should be moved to.
366#[derive(PartialEq, Clone, Copy, Debug, Deserialize, JsonSchema)]
367#[serde(untagged)]
368pub enum GoToDiagnosticSeverityFilter {
369 /// Move to diagnostics of a specific severity.
370 Only(GoToDiagnosticSeverity),
371
372 /// Specify a range of severities to include.
373 Range {
374 /// Minimum severity to move to. Defaults no "error".
375 #[serde(default = "GoToDiagnosticSeverity::min")]
376 min: GoToDiagnosticSeverity,
377 /// Maximum severity to move to. Defaults to "hint".
378 #[serde(default = "GoToDiagnosticSeverity::max")]
379 max: GoToDiagnosticSeverity,
380 },
381}
382
383impl Default for GoToDiagnosticSeverityFilter {
384 fn default() -> Self {
385 Self::Range {
386 min: GoToDiagnosticSeverity::min(),
387 max: GoToDiagnosticSeverity::max(),
388 }
389 }
390}
391
392impl GoToDiagnosticSeverityFilter {
393 pub fn matches(&self, severity: lsp::DiagnosticSeverity) -> bool {
394 let severity: GoToDiagnosticSeverity = severity.into();
395 match self {
396 Self::Only(target) => *target == severity,
397 Self::Range { min, max } => severity >= *min && severity <= *max,
398 }
399 }
400}
401
402#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
403pub struct GitSettings {
404 /// Whether or not to show the git gutter.
405 ///
406 /// Default: tracked_files
407 pub git_gutter: Option<GitGutterSetting>,
408 /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
409 ///
410 /// Default: null
411 pub gutter_debounce: Option<u64>,
412 /// Whether or not to show git blame data inline in
413 /// the currently focused line.
414 ///
415 /// Default: on
416 pub inline_blame: Option<InlineBlameSettings>,
417 /// How hunks are displayed visually in the editor.
418 ///
419 /// Default: staged_hollow
420 pub hunk_style: Option<GitHunkStyleSetting>,
421}
422
423impl GitSettings {
424 pub fn inline_blame_enabled(&self) -> bool {
425 #[allow(unknown_lints, clippy::manual_unwrap_or_default)]
426 match self.inline_blame {
427 Some(InlineBlameSettings { enabled, .. }) => enabled,
428 _ => false,
429 }
430 }
431
432 pub fn inline_blame_delay(&self) -> Option<Duration> {
433 match self.inline_blame {
434 Some(InlineBlameSettings { delay_ms, .. }) if delay_ms > 0 => {
435 Some(Duration::from_millis(delay_ms))
436 }
437 _ => None,
438 }
439 }
440
441 pub fn show_inline_commit_summary(&self) -> bool {
442 match self.inline_blame {
443 Some(InlineBlameSettings {
444 show_commit_summary,
445 ..
446 }) => show_commit_summary,
447 _ => false,
448 }
449 }
450}
451
452#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
453#[serde(rename_all = "snake_case")]
454pub enum GitHunkStyleSetting {
455 /// Show unstaged hunks with a filled background and staged hunks hollow.
456 #[default]
457 StagedHollow,
458 /// Show unstaged hunks hollow and staged hunks with a filled background.
459 UnstagedHollow,
460}
461
462#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
463#[serde(rename_all = "snake_case")]
464pub enum GitGutterSetting {
465 /// Show git gutter in tracked files.
466 #[default]
467 TrackedFiles,
468 /// Hide git gutter
469 Hide,
470}
471
472#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
473#[serde(rename_all = "snake_case")]
474pub struct InlineBlameSettings {
475 /// Whether or not to show git blame data inline in
476 /// the currently focused line.
477 ///
478 /// Default: true
479 #[serde(default = "default_true")]
480 pub enabled: bool,
481 /// Whether to only show the inline blame information
482 /// after a delay once the cursor stops moving.
483 ///
484 /// Default: 0
485 #[serde(default)]
486 pub delay_ms: u64,
487 /// The amount of padding between the end of the source line and the start
488 /// of the inline blame in units of columns.
489 ///
490 /// Default: 7
491 #[serde(default = "default_inline_blame_padding")]
492 pub padding: u32,
493 /// The minimum column number to show the inline blame information at
494 ///
495 /// Default: 0
496 #[serde(default)]
497 pub min_column: u32,
498 /// Whether to show commit summary as part of the inline blame.
499 ///
500 /// Default: false
501 #[serde(default)]
502 pub show_commit_summary: bool,
503}
504
505fn default_inline_blame_padding() -> u32 {
506 7
507}
508
509impl Default for InlineBlameSettings {
510 fn default() -> Self {
511 Self {
512 enabled: true,
513 delay_ms: 0,
514 padding: default_inline_blame_padding(),
515 min_column: 0,
516 show_commit_summary: false,
517 }
518 }
519}
520
521#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
522pub struct BinarySettings {
523 pub path: Option<String>,
524 pub arguments: Option<Vec<String>>,
525 // this can't be an FxHashMap because the extension APIs require the default SipHash
526 pub env: Option<std::collections::HashMap<String, String>>,
527 pub ignore_system_version: Option<bool>,
528}
529
530#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
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}