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