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