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