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