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