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