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