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