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