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