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 /// Which information to show in the branch picker.
409 ///
410 /// Default: on
411 pub branch_picker: Option<BranchPickerSettings>,
412 /// How hunks are displayed visually in the editor.
413 ///
414 /// Default: staged_hollow
415 pub hunk_style: Option<GitHunkStyleSetting>,
416}
417
418impl GitSettings {
419 pub fn inline_blame_enabled(&self) -> bool {
420 #[allow(unknown_lints, clippy::manual_unwrap_or_default)]
421 match self.inline_blame {
422 Some(InlineBlameSettings { enabled, .. }) => enabled,
423 _ => false,
424 }
425 }
426
427 pub fn inline_blame_delay(&self) -> Option<Duration> {
428 match self.inline_blame {
429 Some(InlineBlameSettings { delay_ms, .. }) if delay_ms > 0 => {
430 Some(Duration::from_millis(delay_ms))
431 }
432 _ => None,
433 }
434 }
435
436 pub fn show_inline_commit_summary(&self) -> bool {
437 match self.inline_blame {
438 Some(InlineBlameSettings {
439 show_commit_summary,
440 ..
441 }) => show_commit_summary,
442 _ => false,
443 }
444 }
445}
446
447#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
448#[serde(rename_all = "snake_case")]
449pub enum GitHunkStyleSetting {
450 /// Show unstaged hunks with a filled background and staged hunks hollow.
451 #[default]
452 StagedHollow,
453 /// Show unstaged hunks hollow and staged hunks with a filled background.
454 UnstagedHollow,
455}
456
457#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
458#[serde(rename_all = "snake_case")]
459pub enum GitGutterSetting {
460 /// Show git gutter in tracked files.
461 #[default]
462 TrackedFiles,
463 /// Hide git gutter
464 Hide,
465}
466
467#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
468#[serde(rename_all = "snake_case")]
469pub struct InlineBlameSettings {
470 /// Whether or not to show git blame data inline in
471 /// the currently focused line.
472 ///
473 /// Default: true
474 #[serde(default = "default_true")]
475 pub enabled: bool,
476 /// Whether to only show the inline blame information
477 /// after a delay once the cursor stops moving.
478 ///
479 /// Default: 0
480 #[serde(default)]
481 pub delay_ms: u64,
482 /// The amount of padding between the end of the source line and the start
483 /// of the inline blame in units of columns.
484 ///
485 /// Default: 7
486 #[serde(default = "default_inline_blame_padding")]
487 pub padding: u32,
488 /// The minimum column number to show the inline blame information at
489 ///
490 /// Default: 0
491 #[serde(default)]
492 pub min_column: u32,
493 /// Whether to show commit summary as part of the inline blame.
494 ///
495 /// Default: false
496 #[serde(default)]
497 pub show_commit_summary: bool,
498}
499
500fn default_inline_blame_padding() -> u32 {
501 7
502}
503
504impl Default for InlineBlameSettings {
505 fn default() -> Self {
506 Self {
507 enabled: true,
508 delay_ms: 0,
509 padding: default_inline_blame_padding(),
510 min_column: 0,
511 show_commit_summary: false,
512 }
513 }
514}
515
516#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
517#[serde(rename_all = "snake_case")]
518pub struct BranchPickerSettings {
519 /// Whether to show author name as part of the commit information.
520 ///
521 /// Default: false
522 #[serde(default)]
523 pub show_author_name: bool,
524}
525
526impl Default for BranchPickerSettings {
527 fn default() -> Self {
528 Self {
529 show_author_name: true,
530 }
531 }
532}
533
534#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
535pub struct BinarySettings {
536 pub path: Option<String>,
537 pub arguments: Option<Vec<String>>,
538 pub env: Option<BTreeMap<String, String>>,
539 pub ignore_system_version: Option<bool>,
540}
541
542#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
543pub struct FetchSettings {
544 // Whether to consider pre-releases for fetching
545 pub pre_release: Option<bool>,
546}
547
548#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
549#[serde(rename_all = "snake_case")]
550pub struct LspSettings {
551 pub binary: Option<BinarySettings>,
552 pub initialization_options: Option<serde_json::Value>,
553 pub settings: Option<serde_json::Value>,
554 /// If the server supports sending tasks over LSP extensions,
555 /// this setting can be used to enable or disable them in Zed.
556 /// Default: true
557 #[serde(default = "default_true")]
558 pub enable_lsp_tasks: bool,
559 pub fetch: Option<FetchSettings>,
560}
561
562impl Default for LspSettings {
563 fn default() -> Self {
564 Self {
565 binary: None,
566 initialization_options: None,
567 settings: None,
568 enable_lsp_tasks: true,
569 fetch: None,
570 }
571 }
572}
573
574#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
575pub struct SessionSettings {
576 /// Whether or not to restore unsaved buffers on restart.
577 ///
578 /// If this is true, user won't be prompted whether to save/discard
579 /// dirty files when closing the application.
580 ///
581 /// Default: true
582 pub restore_unsaved_buffers: bool,
583}
584
585impl Default for SessionSettings {
586 fn default() -> Self {
587 Self {
588 restore_unsaved_buffers: true,
589 }
590 }
591}
592
593impl Settings for ProjectSettings {
594 type FileContent = Self;
595
596 fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
597 sources.json_merge()
598 }
599
600 fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
601 // this just sets the binary name instead of a full path so it relies on path lookup
602 // resolving to the one you want
603 vscode.enum_setting(
604 "npm.packageManager",
605 &mut current.node.npm_path,
606 |s| match s {
607 v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
608 _ => None,
609 },
610 );
611
612 if let Some(b) = vscode.read_bool("git.blame.editorDecoration.enabled") {
613 if let Some(blame) = current.git.inline_blame.as_mut() {
614 blame.enabled = b
615 } else {
616 current.git.inline_blame = Some(InlineBlameSettings {
617 enabled: b,
618 ..Default::default()
619 })
620 }
621 }
622
623 #[derive(Deserialize)]
624 struct VsCodeContextServerCommand {
625 command: PathBuf,
626 args: Option<Vec<String>>,
627 env: Option<HashMap<String, String>>,
628 // note: we don't support envFile and type
629 }
630 impl From<VsCodeContextServerCommand> for ContextServerCommand {
631 fn from(cmd: VsCodeContextServerCommand) -> Self {
632 Self {
633 path: cmd.command,
634 args: cmd.args.unwrap_or_default(),
635 env: cmd.env,
636 timeout: None,
637 }
638 }
639 }
640 if let Some(mcp) = vscode.read_value("mcp").and_then(|v| v.as_object()) {
641 current
642 .context_servers
643 .extend(mcp.iter().filter_map(|(k, v)| {
644 Some((
645 k.clone().into(),
646 ContextServerSettings::Custom {
647 enabled: true,
648 command: serde_json::from_value::<VsCodeContextServerCommand>(
649 v.clone(),
650 )
651 .ok()?
652 .into(),
653 },
654 ))
655 }));
656 }
657
658 // TODO: translate lsp settings for rust-analyzer and other popular ones to old.lsp
659 }
660}
661
662pub enum SettingsObserverMode {
663 Local(Arc<dyn Fs>),
664 Remote,
665}
666
667#[derive(Clone, Debug, PartialEq)]
668pub enum SettingsObserverEvent {
669 LocalSettingsUpdated(Result<PathBuf, InvalidSettingsError>),
670 LocalTasksUpdated(Result<PathBuf, InvalidSettingsError>),
671 LocalDebugScenariosUpdated(Result<PathBuf, InvalidSettingsError>),
672}
673
674impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
675
676pub struct SettingsObserver {
677 mode: SettingsObserverMode,
678 downstream_client: Option<AnyProtoClient>,
679 worktree_store: Entity<WorktreeStore>,
680 project_id: u64,
681 task_store: Entity<TaskStore>,
682 _user_settings_watcher: Option<Subscription>,
683 _global_task_config_watcher: Task<()>,
684 _global_debug_config_watcher: Task<()>,
685}
686
687/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
688/// (or the equivalent protobuf messages from upstream) and updates local settings
689/// and sends notifications downstream.
690/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
691/// upstream.
692impl SettingsObserver {
693 pub fn init(client: &AnyProtoClient) {
694 client.add_entity_message_handler(Self::handle_update_worktree_settings);
695 client.add_entity_message_handler(Self::handle_update_user_settings);
696 }
697
698 pub fn new_local(
699 fs: Arc<dyn Fs>,
700 worktree_store: Entity<WorktreeStore>,
701 task_store: Entity<TaskStore>,
702 cx: &mut Context<Self>,
703 ) -> Self {
704 cx.subscribe(&worktree_store, Self::on_worktree_store_event)
705 .detach();
706
707 Self {
708 worktree_store,
709 task_store,
710 mode: SettingsObserverMode::Local(fs.clone()),
711 downstream_client: None,
712 _user_settings_watcher: None,
713 project_id: REMOTE_SERVER_PROJECT_ID,
714 _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
715 fs.clone(),
716 paths::tasks_file().clone(),
717 cx,
718 ),
719 _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
720 fs.clone(),
721 paths::debug_scenarios_file().clone(),
722 cx,
723 ),
724 }
725 }
726
727 pub fn new_remote(
728 fs: Arc<dyn Fs>,
729 worktree_store: Entity<WorktreeStore>,
730 task_store: Entity<TaskStore>,
731 upstream_client: Option<AnyProtoClient>,
732 cx: &mut Context<Self>,
733 ) -> Self {
734 let mut user_settings_watcher = None;
735 if cx.try_global::<SettingsStore>().is_some() {
736 if let Some(upstream_client) = upstream_client {
737 let mut user_settings = None;
738 user_settings_watcher = Some(cx.observe_global::<SettingsStore>(move |_, cx| {
739 let new_settings = cx.global::<SettingsStore>().raw_user_settings();
740 if Some(new_settings) != user_settings.as_ref() {
741 if let Some(new_settings_string) = serde_json::to_string(new_settings).ok()
742 {
743 user_settings = Some(new_settings.clone());
744 upstream_client
745 .send(proto::UpdateUserSettings {
746 project_id: REMOTE_SERVER_PROJECT_ID,
747 contents: new_settings_string,
748 })
749 .log_err();
750 }
751 }
752 }));
753 }
754 };
755
756 Self {
757 worktree_store,
758 task_store,
759 mode: SettingsObserverMode::Remote,
760 downstream_client: None,
761 project_id: REMOTE_SERVER_PROJECT_ID,
762 _user_settings_watcher: user_settings_watcher,
763 _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
764 fs.clone(),
765 paths::tasks_file().clone(),
766 cx,
767 ),
768 _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
769 fs.clone(),
770 paths::debug_scenarios_file().clone(),
771 cx,
772 ),
773 }
774 }
775
776 pub fn shared(
777 &mut self,
778 project_id: u64,
779 downstream_client: AnyProtoClient,
780 cx: &mut Context<Self>,
781 ) {
782 self.project_id = project_id;
783 self.downstream_client = Some(downstream_client.clone());
784
785 let store = cx.global::<SettingsStore>();
786 for worktree in self.worktree_store.read(cx).worktrees() {
787 let worktree_id = worktree.read(cx).id().to_proto();
788 for (path, content) in store.local_settings(worktree.read(cx).id()) {
789 downstream_client
790 .send(proto::UpdateWorktreeSettings {
791 project_id,
792 worktree_id,
793 path: path.to_proto(),
794 content: Some(content),
795 kind: Some(
796 local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
797 ),
798 })
799 .log_err();
800 }
801 for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
802 downstream_client
803 .send(proto::UpdateWorktreeSettings {
804 project_id,
805 worktree_id,
806 path: path.to_proto(),
807 content: Some(content),
808 kind: Some(
809 local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
810 ),
811 })
812 .log_err();
813 }
814 }
815 }
816
817 pub fn unshared(&mut self, _: &mut Context<Self>) {
818 self.downstream_client = None;
819 }
820
821 async fn handle_update_worktree_settings(
822 this: Entity<Self>,
823 envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
824 mut cx: AsyncApp,
825 ) -> anyhow::Result<()> {
826 let kind = match envelope.payload.kind {
827 Some(kind) => proto::LocalSettingsKind::from_i32(kind)
828 .with_context(|| format!("unknown kind {kind}"))?,
829 None => proto::LocalSettingsKind::Settings,
830 };
831 this.update(&mut cx, |this, cx| {
832 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
833 let Some(worktree) = this
834 .worktree_store
835 .read(cx)
836 .worktree_for_id(worktree_id, cx)
837 else {
838 return;
839 };
840
841 this.update_settings(
842 worktree,
843 [(
844 Arc::<Path>::from_proto(envelope.payload.path.clone()),
845 local_settings_kind_from_proto(kind),
846 envelope.payload.content,
847 )],
848 cx,
849 );
850 })?;
851 Ok(())
852 }
853
854 async fn handle_update_user_settings(
855 _: Entity<Self>,
856 envelope: TypedEnvelope<proto::UpdateUserSettings>,
857 cx: AsyncApp,
858 ) -> anyhow::Result<()> {
859 let new_settings = serde_json::from_str::<serde_json::Value>(&envelope.payload.contents)
860 .with_context(|| {
861 format!("deserializing {} user settings", envelope.payload.contents)
862 })?;
863 cx.update_global(|settings_store: &mut SettingsStore, cx| {
864 settings_store
865 .set_raw_user_settings(new_settings, cx)
866 .context("setting new user settings")?;
867 anyhow::Ok(())
868 })??;
869 Ok(())
870 }
871
872 fn on_worktree_store_event(
873 &mut self,
874 _: Entity<WorktreeStore>,
875 event: &WorktreeStoreEvent,
876 cx: &mut Context<Self>,
877 ) {
878 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
879 cx.subscribe(worktree, |this, worktree, event, cx| {
880 if let worktree::Event::UpdatedEntries(changes) = event {
881 this.update_local_worktree_settings(&worktree, changes, cx)
882 }
883 })
884 .detach()
885 }
886 }
887
888 fn update_local_worktree_settings(
889 &mut self,
890 worktree: &Entity<Worktree>,
891 changes: &UpdatedEntriesSet,
892 cx: &mut Context<Self>,
893 ) {
894 let SettingsObserverMode::Local(fs) = &self.mode else {
895 return;
896 };
897
898 let mut settings_contents = Vec::new();
899 for (path, _, change) in changes.iter() {
900 let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
901 let settings_dir = Arc::<Path>::from(
902 path.ancestors()
903 .nth(local_settings_file_relative_path().components().count())
904 .unwrap(),
905 );
906 (settings_dir, LocalSettingsKind::Settings)
907 } else if path.ends_with(local_tasks_file_relative_path()) {
908 let settings_dir = Arc::<Path>::from(
909 path.ancestors()
910 .nth(
911 local_tasks_file_relative_path()
912 .components()
913 .count()
914 .saturating_sub(1),
915 )
916 .unwrap(),
917 );
918 (settings_dir, LocalSettingsKind::Tasks)
919 } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
920 let settings_dir = Arc::<Path>::from(
921 path.ancestors()
922 .nth(
923 local_vscode_tasks_file_relative_path()
924 .components()
925 .count()
926 .saturating_sub(1),
927 )
928 .unwrap(),
929 );
930 (settings_dir, LocalSettingsKind::Tasks)
931 } else if path.ends_with(local_debug_file_relative_path()) {
932 let settings_dir = Arc::<Path>::from(
933 path.ancestors()
934 .nth(
935 local_debug_file_relative_path()
936 .components()
937 .count()
938 .saturating_sub(1),
939 )
940 .unwrap(),
941 );
942 (settings_dir, LocalSettingsKind::Debug)
943 } else if path.ends_with(local_vscode_launch_file_relative_path()) {
944 let settings_dir = Arc::<Path>::from(
945 path.ancestors()
946 .nth(
947 local_vscode_tasks_file_relative_path()
948 .components()
949 .count()
950 .saturating_sub(1),
951 )
952 .unwrap(),
953 );
954 (settings_dir, LocalSettingsKind::Debug)
955 } else if path.ends_with(EDITORCONFIG_NAME) {
956 let Some(settings_dir) = path.parent().map(Arc::from) else {
957 continue;
958 };
959 (settings_dir, LocalSettingsKind::Editorconfig)
960 } else {
961 continue;
962 };
963
964 let removed = change == &PathChange::Removed;
965 let fs = fs.clone();
966 let abs_path = match worktree.read(cx).absolutize(path) {
967 Ok(abs_path) => abs_path,
968 Err(e) => {
969 log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
970 continue;
971 }
972 };
973 settings_contents.push(async move {
974 (
975 settings_dir,
976 kind,
977 if removed {
978 None
979 } else {
980 Some(
981 async move {
982 let content = fs.load(&abs_path).await?;
983 if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
984 let vscode_tasks =
985 parse_json_with_comments::<VsCodeTaskFile>(&content)
986 .with_context(|| {
987 format!("parsing VSCode tasks, file {abs_path:?}")
988 })?;
989 let zed_tasks = TaskTemplates::try_from(vscode_tasks)
990 .with_context(|| {
991 format!(
992 "converting VSCode tasks into Zed ones, file {abs_path:?}"
993 )
994 })?;
995 serde_json::to_string(&zed_tasks).with_context(|| {
996 format!(
997 "serializing Zed tasks into JSON, file {abs_path:?}"
998 )
999 })
1000 } else if abs_path.ends_with(local_vscode_launch_file_relative_path()) {
1001 let vscode_tasks =
1002 parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
1003 .with_context(|| {
1004 format!("parsing VSCode debug tasks, file {abs_path:?}")
1005 })?;
1006 let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
1007 .with_context(|| {
1008 format!(
1009 "converting VSCode debug tasks into Zed ones, file {abs_path:?}"
1010 )
1011 })?;
1012 serde_json::to_string(&zed_tasks).with_context(|| {
1013 format!(
1014 "serializing Zed tasks into JSON, file {abs_path:?}"
1015 )
1016 })
1017 } else {
1018 Ok(content)
1019 }
1020 }
1021 .await,
1022 )
1023 },
1024 )
1025 });
1026 }
1027
1028 if settings_contents.is_empty() {
1029 return;
1030 }
1031
1032 let worktree = worktree.clone();
1033 cx.spawn(async move |this, cx| {
1034 let settings_contents: Vec<(Arc<Path>, _, _)> =
1035 futures::future::join_all(settings_contents).await;
1036 cx.update(|cx| {
1037 this.update(cx, |this, cx| {
1038 this.update_settings(
1039 worktree,
1040 settings_contents.into_iter().map(|(path, kind, content)| {
1041 (path, kind, content.and_then(|c| c.log_err()))
1042 }),
1043 cx,
1044 )
1045 })
1046 })
1047 })
1048 .detach();
1049 }
1050
1051 fn update_settings(
1052 &mut self,
1053 worktree: Entity<Worktree>,
1054 settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
1055 cx: &mut Context<Self>,
1056 ) {
1057 let worktree_id = worktree.read(cx).id();
1058 let remote_worktree_id = worktree.read(cx).id();
1059 let task_store = self.task_store.clone();
1060
1061 for (directory, kind, file_content) in settings_contents {
1062 match kind {
1063 LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
1064 .update_global::<SettingsStore, _>(|store, cx| {
1065 let result = store.set_local_settings(
1066 worktree_id,
1067 directory.clone(),
1068 kind,
1069 file_content.as_deref(),
1070 cx,
1071 );
1072
1073 match result {
1074 Err(InvalidSettingsError::LocalSettings { path, message }) => {
1075 log::error!("Failed to set local settings in {path:?}: {message}");
1076 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
1077 InvalidSettingsError::LocalSettings { path, message },
1078 )));
1079 }
1080 Err(e) => {
1081 log::error!("Failed to set local settings: {e}");
1082 }
1083 Ok(()) => {
1084 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(
1085 directory.join(local_settings_file_relative_path())
1086 )));
1087 }
1088 }
1089 }),
1090 LocalSettingsKind::Tasks => {
1091 let result = task_store.update(cx, |task_store, cx| {
1092 task_store.update_user_tasks(
1093 TaskSettingsLocation::Worktree(SettingsLocation {
1094 worktree_id,
1095 path: directory.as_ref(),
1096 }),
1097 file_content.as_deref(),
1098 cx,
1099 )
1100 });
1101
1102 match result {
1103 Err(InvalidSettingsError::Tasks { path, message }) => {
1104 log::error!("Failed to set local tasks in {path:?}: {message:?}");
1105 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1106 InvalidSettingsError::Tasks { path, message },
1107 )));
1108 }
1109 Err(e) => {
1110 log::error!("Failed to set local tasks: {e}");
1111 }
1112 Ok(()) => {
1113 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1114 directory.join(task_file_name())
1115 )));
1116 }
1117 }
1118 }
1119 LocalSettingsKind::Debug => {
1120 let result = task_store.update(cx, |task_store, cx| {
1121 task_store.update_user_debug_scenarios(
1122 TaskSettingsLocation::Worktree(SettingsLocation {
1123 worktree_id,
1124 path: directory.as_ref(),
1125 }),
1126 file_content.as_deref(),
1127 cx,
1128 )
1129 });
1130
1131 match result {
1132 Err(InvalidSettingsError::Debug { path, message }) => {
1133 log::error!(
1134 "Failed to set local debug scenarios in {path:?}: {message:?}"
1135 );
1136 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1137 InvalidSettingsError::Debug { path, message },
1138 )));
1139 }
1140 Err(e) => {
1141 log::error!("Failed to set local tasks: {e}");
1142 }
1143 Ok(()) => {
1144 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1145 directory.join(task_file_name())
1146 )));
1147 }
1148 }
1149 }
1150 };
1151
1152 if let Some(downstream_client) = &self.downstream_client {
1153 downstream_client
1154 .send(proto::UpdateWorktreeSettings {
1155 project_id: self.project_id,
1156 worktree_id: remote_worktree_id.to_proto(),
1157 path: directory.to_proto(),
1158 content: file_content.clone(),
1159 kind: Some(local_settings_kind_to_proto(kind).into()),
1160 })
1161 .log_err();
1162 }
1163 }
1164 }
1165
1166 fn subscribe_to_global_task_file_changes(
1167 fs: Arc<dyn Fs>,
1168 file_path: PathBuf,
1169 cx: &mut Context<Self>,
1170 ) -> Task<()> {
1171 let mut user_tasks_file_rx =
1172 watch_config_file(cx.background_executor(), fs, file_path.clone());
1173 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1174 let weak_entry = cx.weak_entity();
1175 cx.spawn(async move |settings_observer, cx| {
1176 let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1177 settings_observer.task_store.clone()
1178 }) else {
1179 return;
1180 };
1181 if let Some(user_tasks_content) = user_tasks_content {
1182 let Ok(()) = task_store.update(cx, |task_store, cx| {
1183 task_store
1184 .update_user_tasks(
1185 TaskSettingsLocation::Global(&file_path),
1186 Some(&user_tasks_content),
1187 cx,
1188 )
1189 .log_err();
1190 }) else {
1191 return;
1192 };
1193 }
1194 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1195 let Ok(result) = task_store.update(cx, |task_store, cx| {
1196 task_store.update_user_tasks(
1197 TaskSettingsLocation::Global(&file_path),
1198 Some(&user_tasks_content),
1199 cx,
1200 )
1201 }) else {
1202 break;
1203 };
1204
1205 weak_entry
1206 .update(cx, |_, cx| match result {
1207 Ok(()) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1208 file_path.clone()
1209 ))),
1210 Err(err) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1211 InvalidSettingsError::Tasks {
1212 path: file_path.clone(),
1213 message: err.to_string(),
1214 },
1215 ))),
1216 })
1217 .ok();
1218 }
1219 })
1220 }
1221 fn subscribe_to_global_debug_scenarios_changes(
1222 fs: Arc<dyn Fs>,
1223 file_path: PathBuf,
1224 cx: &mut Context<Self>,
1225 ) -> Task<()> {
1226 let mut user_tasks_file_rx =
1227 watch_config_file(cx.background_executor(), fs, file_path.clone());
1228 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1229 let weak_entry = cx.weak_entity();
1230 cx.spawn(async move |settings_observer, cx| {
1231 let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1232 settings_observer.task_store.clone()
1233 }) else {
1234 return;
1235 };
1236 if let Some(user_tasks_content) = user_tasks_content {
1237 let Ok(()) = task_store.update(cx, |task_store, cx| {
1238 task_store
1239 .update_user_debug_scenarios(
1240 TaskSettingsLocation::Global(&file_path),
1241 Some(&user_tasks_content),
1242 cx,
1243 )
1244 .log_err();
1245 }) else {
1246 return;
1247 };
1248 }
1249 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1250 let Ok(result) = task_store.update(cx, |task_store, cx| {
1251 task_store.update_user_debug_scenarios(
1252 TaskSettingsLocation::Global(&file_path),
1253 Some(&user_tasks_content),
1254 cx,
1255 )
1256 }) else {
1257 break;
1258 };
1259
1260 weak_entry
1261 .update(cx, |_, cx| match result {
1262 Ok(()) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(Ok(
1263 file_path.clone(),
1264 ))),
1265 Err(err) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(
1266 Err(InvalidSettingsError::Tasks {
1267 path: file_path.clone(),
1268 message: err.to_string(),
1269 }),
1270 )),
1271 })
1272 .ok();
1273 }
1274 })
1275 }
1276}
1277
1278pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
1279 match kind {
1280 proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
1281 proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
1282 proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
1283 proto::LocalSettingsKind::Debug => LocalSettingsKind::Debug,
1284 }
1285}
1286
1287pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
1288 match kind {
1289 LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
1290 LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
1291 LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
1292 LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug,
1293 }
1294}