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