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 to show the git gutter.
336 ///
337 /// Default: tracked_files
338 pub git_gutter: settings::GitGutterSetting,
339 /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
340 ///
341 /// Default: 0
342 pub gutter_debounce: u64,
343 /// Whether or not to show git blame data inline in
344 /// the currently focused line.
345 ///
346 /// Default: on
347 pub inline_blame: InlineBlameSettings,
348 /// Git blame settings.
349 pub blame: BlameSettings,
350 /// Which information to show in the branch picker.
351 ///
352 /// Default: on
353 pub branch_picker: BranchPickerSettings,
354 /// How hunks are displayed visually in the editor.
355 ///
356 /// Default: staged_hollow
357 pub hunk_style: settings::GitHunkStyleSetting,
358 /// How file paths are displayed in the git gutter.
359 ///
360 /// Default: file_name_first
361 pub path_style: GitPathStyle,
362}
363
364#[derive(Clone, Copy, Debug, PartialEq, Default)]
365pub enum GitPathStyle {
366 #[default]
367 FileNameFirst,
368 FilePathFirst,
369}
370
371impl From<settings::GitPathStyle> for GitPathStyle {
372 fn from(style: settings::GitPathStyle) -> Self {
373 match style {
374 settings::GitPathStyle::FileNameFirst => GitPathStyle::FileNameFirst,
375 settings::GitPathStyle::FilePathFirst => GitPathStyle::FilePathFirst,
376 }
377 }
378}
379
380#[derive(Clone, Copy, Debug)]
381pub struct InlineBlameSettings {
382 /// Whether or not to show git blame data inline in
383 /// the currently focused line.
384 ///
385 /// Default: true
386 pub enabled: bool,
387 /// Whether to only show the inline blame information
388 /// after a delay once the cursor stops moving.
389 ///
390 /// Default: 0
391 pub delay_ms: settings::DelayMs,
392 /// The amount of padding between the end of the source line and the start
393 /// of the inline blame in units of columns.
394 ///
395 /// Default: 7
396 pub padding: u32,
397 /// The minimum column number to show the inline blame information at
398 ///
399 /// Default: 0
400 pub min_column: u32,
401 /// Whether to show commit summary as part of the inline blame.
402 ///
403 /// Default: false
404 pub show_commit_summary: bool,
405}
406
407#[derive(Clone, Copy, Debug)]
408pub struct BlameSettings {
409 /// Whether to show the avatar of the author of the commit.
410 ///
411 /// Default: true
412 pub show_avatar: bool,
413}
414
415impl GitSettings {
416 pub fn inline_blame_delay(&self) -> Option<Duration> {
417 if self.inline_blame.delay_ms.0 > 0 {
418 Some(Duration::from_millis(self.inline_blame.delay_ms.0))
419 } else {
420 None
421 }
422 }
423}
424
425#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
426#[serde(rename_all = "snake_case")]
427pub struct BranchPickerSettings {
428 /// Whether to show author name as part of the commit information.
429 ///
430 /// Default: false
431 #[serde(default)]
432 pub show_author_name: bool,
433}
434
435impl Default for BranchPickerSettings {
436 fn default() -> Self {
437 Self {
438 show_author_name: true,
439 }
440 }
441}
442
443#[derive(Clone, Debug)]
444pub struct DiagnosticsSettings {
445 /// Whether to show the project diagnostics button in the status bar.
446 pub button: bool,
447
448 /// Whether or not to include warning diagnostics.
449 pub include_warnings: bool,
450
451 /// Settings for using LSP pull diagnostics mechanism in Zed.
452 pub lsp_pull_diagnostics: LspPullDiagnosticsSettings,
453
454 /// Settings for showing inline diagnostics.
455 pub inline: InlineDiagnosticsSettings,
456}
457
458#[derive(Clone, Copy, Debug, PartialEq, Eq)]
459pub struct InlineDiagnosticsSettings {
460 /// Whether or not to show inline diagnostics
461 ///
462 /// Default: false
463 pub enabled: bool,
464 /// Whether to only show the inline diagnostics after a delay after the
465 /// last editor event.
466 ///
467 /// Default: 150
468 pub update_debounce_ms: u64,
469 /// The amount of padding between the end of the source line and the start
470 /// of the inline diagnostic in units of columns.
471 ///
472 /// Default: 4
473 pub padding: u32,
474 /// The minimum column to display inline diagnostics. This setting can be
475 /// used to horizontally align inline diagnostics at some position. Lines
476 /// longer than this value will still push diagnostics further to the right.
477 ///
478 /// Default: 0
479 pub min_column: u32,
480
481 pub max_severity: Option<DiagnosticSeverity>,
482}
483
484#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
485pub struct LspPullDiagnosticsSettings {
486 /// Whether to pull for diagnostics or not.
487 ///
488 /// Default: true
489 pub enabled: bool,
490 /// Minimum time to wait before pulling diagnostics from the language server(s).
491 /// 0 turns the debounce off.
492 ///
493 /// Default: 50
494 pub debounce_ms: u64,
495}
496
497impl Settings for ProjectSettings {
498 fn from_settings(content: &settings::SettingsContent) -> Self {
499 let project = &content.project.clone();
500 let diagnostics = content.diagnostics.as_ref().unwrap();
501 let lsp_pull_diagnostics = diagnostics.lsp_pull_diagnostics.as_ref().unwrap();
502 let inline_diagnostics = diagnostics.inline.as_ref().unwrap();
503
504 let git = content.git.as_ref().unwrap();
505 let git_settings = GitSettings {
506 git_gutter: git.git_gutter.unwrap(),
507 gutter_debounce: git.gutter_debounce.unwrap_or_default(),
508 inline_blame: {
509 let inline = git.inline_blame.unwrap();
510 InlineBlameSettings {
511 enabled: inline.enabled.unwrap(),
512 delay_ms: inline.delay_ms.unwrap(),
513 padding: inline.padding.unwrap(),
514 min_column: inline.min_column.unwrap(),
515 show_commit_summary: inline.show_commit_summary.unwrap(),
516 }
517 },
518 blame: {
519 let blame = git.blame.unwrap();
520 BlameSettings {
521 show_avatar: blame.show_avatar.unwrap(),
522 }
523 },
524 branch_picker: {
525 let branch_picker = git.branch_picker.unwrap();
526 BranchPickerSettings {
527 show_author_name: branch_picker.show_author_name.unwrap(),
528 }
529 },
530 hunk_style: git.hunk_style.unwrap(),
531 path_style: git.path_style.unwrap().into(),
532 };
533 Self {
534 context_servers: project
535 .context_servers
536 .clone()
537 .into_iter()
538 .map(|(key, value)| (key, value.into()))
539 .collect(),
540 lsp: project
541 .lsp
542 .clone()
543 .into_iter()
544 .map(|(key, value)| (LanguageServerName(key.into()), value))
545 .collect(),
546 global_lsp_settings: GlobalLspSettings {
547 button: content
548 .global_lsp_settings
549 .as_ref()
550 .unwrap()
551 .button
552 .unwrap(),
553 },
554 dap: project
555 .dap
556 .clone()
557 .into_iter()
558 .map(|(key, value)| (DebugAdapterName(key.into()), DapSettings::from(value)))
559 .collect(),
560 diagnostics: DiagnosticsSettings {
561 button: diagnostics.button.unwrap(),
562 include_warnings: diagnostics.include_warnings.unwrap(),
563 lsp_pull_diagnostics: LspPullDiagnosticsSettings {
564 enabled: lsp_pull_diagnostics.enabled.unwrap(),
565 debounce_ms: lsp_pull_diagnostics.debounce_ms.unwrap().0,
566 },
567 inline: InlineDiagnosticsSettings {
568 enabled: inline_diagnostics.enabled.unwrap(),
569 update_debounce_ms: inline_diagnostics.update_debounce_ms.unwrap().0,
570 padding: inline_diagnostics.padding.unwrap(),
571 min_column: inline_diagnostics.min_column.unwrap(),
572 max_severity: inline_diagnostics.max_severity.map(Into::into),
573 },
574 },
575 git: git_settings,
576 node: content.node.clone().unwrap().into(),
577 load_direnv: project.load_direnv.clone().unwrap(),
578 session: SessionSettings {
579 restore_unsaved_buffers: content.session.unwrap().restore_unsaved_buffers.unwrap(),
580 trust_all_worktrees: content.session.unwrap().trust_all_worktrees.unwrap(),
581 },
582 }
583 }
584}
585
586pub enum SettingsObserverMode {
587 Local(Arc<dyn Fs>),
588 Remote,
589}
590
591#[derive(Clone, Debug, PartialEq)]
592pub enum SettingsObserverEvent {
593 LocalSettingsUpdated(Result<PathBuf, InvalidSettingsError>),
594 LocalTasksUpdated(Result<PathBuf, InvalidSettingsError>),
595 LocalDebugScenariosUpdated(Result<PathBuf, InvalidSettingsError>),
596}
597
598impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
599
600pub struct SettingsObserver {
601 mode: SettingsObserverMode,
602 downstream_client: Option<AnyProtoClient>,
603 worktree_store: Entity<WorktreeStore>,
604 project_id: u64,
605 task_store: Entity<TaskStore>,
606 pending_local_settings:
607 HashMap<PathTrust, BTreeMap<(WorktreeId, Arc<RelPath>), Option<String>>>,
608 _trusted_worktrees_watcher: Option<Subscription>,
609 _user_settings_watcher: Option<Subscription>,
610 _global_task_config_watcher: Task<()>,
611 _global_debug_config_watcher: Task<()>,
612}
613
614/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
615/// (or the equivalent protobuf messages from upstream) and updates local settings
616/// and sends notifications downstream.
617/// In ssh mode it also monitors ~/.config/zed/{settings, task}.json and sends the content
618/// upstream.
619impl SettingsObserver {
620 pub fn init(client: &AnyProtoClient) {
621 client.add_entity_message_handler(Self::handle_update_worktree_settings);
622 client.add_entity_message_handler(Self::handle_update_user_settings);
623 }
624
625 pub fn new_local(
626 fs: Arc<dyn Fs>,
627 worktree_store: Entity<WorktreeStore>,
628 task_store: Entity<TaskStore>,
629 cx: &mut Context<Self>,
630 ) -> Self {
631 cx.subscribe(&worktree_store, Self::on_worktree_store_event)
632 .detach();
633
634 let _trusted_worktrees_watcher =
635 TrustedWorktrees::try_get_global(cx).map(|trusted_worktrees| {
636 cx.subscribe(
637 &trusted_worktrees,
638 move |settings_observer, _, e, cx| match e {
639 TrustedWorktreesEvent::Trusted(_, trusted_paths) => {
640 for trusted_path in trusted_paths {
641 if let Some(pending_local_settings) = settings_observer
642 .pending_local_settings
643 .remove(trusted_path)
644 {
645 for ((worktree_id, directory_path), settings_contents) in
646 pending_local_settings
647 {
648 apply_local_settings(
649 worktree_id,
650 &directory_path,
651 LocalSettingsKind::Settings,
652 &settings_contents,
653 cx,
654 );
655 if let Some(downstream_client) =
656 &settings_observer.downstream_client
657 {
658 downstream_client
659 .send(proto::UpdateWorktreeSettings {
660 project_id: settings_observer.project_id,
661 worktree_id: worktree_id.to_proto(),
662 path: directory_path.to_proto(),
663 content: settings_contents,
664 kind: Some(
665 local_settings_kind_to_proto(
666 LocalSettingsKind::Settings,
667 )
668 .into(),
669 ),
670 })
671 .log_err();
672 }
673 }
674 }
675 }
676 }
677 TrustedWorktreesEvent::Restricted(..) => {}
678 },
679 )
680 });
681
682 Self {
683 worktree_store,
684 task_store,
685 mode: SettingsObserverMode::Local(fs.clone()),
686 downstream_client: None,
687 _trusted_worktrees_watcher,
688 pending_local_settings: HashMap::default(),
689 _user_settings_watcher: None,
690 project_id: REMOTE_SERVER_PROJECT_ID,
691 _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
692 fs.clone(),
693 paths::tasks_file().clone(),
694 cx,
695 ),
696 _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
697 fs.clone(),
698 paths::debug_scenarios_file().clone(),
699 cx,
700 ),
701 }
702 }
703
704 pub fn new_remote(
705 fs: Arc<dyn Fs>,
706 worktree_store: Entity<WorktreeStore>,
707 task_store: Entity<TaskStore>,
708 upstream_client: Option<AnyProtoClient>,
709 cx: &mut Context<Self>,
710 ) -> Self {
711 let mut user_settings_watcher = None;
712 if cx.try_global::<SettingsStore>().is_some() {
713 if let Some(upstream_client) = upstream_client {
714 let mut user_settings = None;
715 user_settings_watcher = Some(cx.observe_global::<SettingsStore>(move |_, cx| {
716 if let Some(new_settings) = cx.global::<SettingsStore>().raw_user_settings() {
717 if Some(new_settings) != user_settings.as_ref() {
718 if let Some(new_settings_string) =
719 serde_json::to_string(new_settings).ok()
720 {
721 user_settings = Some(new_settings.clone());
722 upstream_client
723 .send(proto::UpdateUserSettings {
724 project_id: REMOTE_SERVER_PROJECT_ID,
725 contents: new_settings_string,
726 })
727 .log_err();
728 }
729 }
730 }
731 }));
732 }
733 };
734
735 Self {
736 worktree_store,
737 task_store,
738 mode: SettingsObserverMode::Remote,
739 downstream_client: None,
740 project_id: REMOTE_SERVER_PROJECT_ID,
741 _trusted_worktrees_watcher: None,
742 pending_local_settings: HashMap::default(),
743 _user_settings_watcher: user_settings_watcher,
744 _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
745 fs.clone(),
746 paths::tasks_file().clone(),
747 cx,
748 ),
749 _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
750 fs.clone(),
751 paths::debug_scenarios_file().clone(),
752 cx,
753 ),
754 }
755 }
756
757 pub fn shared(
758 &mut self,
759 project_id: u64,
760 downstream_client: AnyProtoClient,
761 cx: &mut Context<Self>,
762 ) {
763 self.project_id = project_id;
764 self.downstream_client = Some(downstream_client.clone());
765
766 let store = cx.global::<SettingsStore>();
767 for worktree in self.worktree_store.read(cx).worktrees() {
768 let worktree_id = worktree.read(cx).id().to_proto();
769 for (path, content) in store.local_settings(worktree.read(cx).id()) {
770 let content = serde_json::to_string(&content).unwrap();
771 downstream_client
772 .send(proto::UpdateWorktreeSettings {
773 project_id,
774 worktree_id,
775 path: path.to_proto(),
776 content: Some(content),
777 kind: Some(
778 local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
779 ),
780 })
781 .log_err();
782 }
783 for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
784 downstream_client
785 .send(proto::UpdateWorktreeSettings {
786 project_id,
787 worktree_id,
788 path: path.to_proto(),
789 content: Some(content),
790 kind: Some(
791 local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
792 ),
793 })
794 .log_err();
795 }
796 }
797 }
798
799 pub fn unshared(&mut self, _: &mut Context<Self>) {
800 self.downstream_client = None;
801 }
802
803 async fn handle_update_worktree_settings(
804 this: Entity<Self>,
805 envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
806 mut cx: AsyncApp,
807 ) -> anyhow::Result<()> {
808 let kind = match envelope.payload.kind {
809 Some(kind) => proto::LocalSettingsKind::from_i32(kind)
810 .with_context(|| format!("unknown kind {kind}"))?,
811 None => proto::LocalSettingsKind::Settings,
812 };
813 let path = RelPath::from_proto(&envelope.payload.path)?;
814 this.update(&mut cx, |this, cx| {
815 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
816 let Some(worktree) = this
817 .worktree_store
818 .read(cx)
819 .worktree_for_id(worktree_id, cx)
820 else {
821 return;
822 };
823
824 this.update_settings(
825 worktree,
826 [(
827 path,
828 local_settings_kind_from_proto(kind),
829 envelope.payload.content,
830 )],
831 cx,
832 );
833 })?;
834 Ok(())
835 }
836
837 async fn handle_update_user_settings(
838 _: Entity<Self>,
839 envelope: TypedEnvelope<proto::UpdateUserSettings>,
840 cx: AsyncApp,
841 ) -> anyhow::Result<()> {
842 cx.update_global(|settings_store: &mut SettingsStore, cx| {
843 settings_store
844 .set_user_settings(&envelope.payload.contents, cx)
845 .result()
846 .context("setting new user settings")?;
847 anyhow::Ok(())
848 })??;
849 Ok(())
850 }
851
852 fn on_worktree_store_event(
853 &mut self,
854 _: Entity<WorktreeStore>,
855 event: &WorktreeStoreEvent,
856 cx: &mut Context<Self>,
857 ) {
858 match event {
859 WorktreeStoreEvent::WorktreeAdded(worktree) => cx
860 .subscribe(worktree, |this, worktree, event, cx| {
861 if let worktree::Event::UpdatedEntries(changes) = event {
862 this.update_local_worktree_settings(&worktree, changes, cx)
863 }
864 })
865 .detach(),
866 WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) => {
867 cx.update_global::<SettingsStore, _>(|store, cx| {
868 store.clear_local_settings(*worktree_id, cx).log_err();
869 });
870 }
871 _ => {}
872 }
873 }
874
875 fn update_local_worktree_settings(
876 &mut self,
877 worktree: &Entity<Worktree>,
878 changes: &UpdatedEntriesSet,
879 cx: &mut Context<Self>,
880 ) {
881 let SettingsObserverMode::Local(fs) = &self.mode else {
882 return;
883 };
884
885 let mut settings_contents = Vec::new();
886 for (path, _, change) in changes.iter() {
887 let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
888 let settings_dir = path
889 .ancestors()
890 .nth(local_settings_file_relative_path().components().count())
891 .unwrap()
892 .into();
893 (settings_dir, LocalSettingsKind::Settings)
894 } else if path.ends_with(local_tasks_file_relative_path()) {
895 let settings_dir = path
896 .ancestors()
897 .nth(
898 local_tasks_file_relative_path()
899 .components()
900 .count()
901 .saturating_sub(1),
902 )
903 .unwrap()
904 .into();
905 (settings_dir, LocalSettingsKind::Tasks)
906 } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
907 let settings_dir = path
908 .ancestors()
909 .nth(
910 local_vscode_tasks_file_relative_path()
911 .components()
912 .count()
913 .saturating_sub(1),
914 )
915 .unwrap()
916 .into();
917 (settings_dir, LocalSettingsKind::Tasks)
918 } else if path.ends_with(local_debug_file_relative_path()) {
919 let settings_dir = path
920 .ancestors()
921 .nth(
922 local_debug_file_relative_path()
923 .components()
924 .count()
925 .saturating_sub(1),
926 )
927 .unwrap()
928 .into();
929 (settings_dir, LocalSettingsKind::Debug)
930 } else if path.ends_with(local_vscode_launch_file_relative_path()) {
931 let settings_dir = path
932 .ancestors()
933 .nth(
934 local_vscode_tasks_file_relative_path()
935 .components()
936 .count()
937 .saturating_sub(1),
938 )
939 .unwrap()
940 .into();
941 (settings_dir, LocalSettingsKind::Debug)
942 } else if path.ends_with(RelPath::unix(EDITORCONFIG_NAME).unwrap()) {
943 let Some(settings_dir) = path.parent().map(Arc::from) else {
944 continue;
945 };
946 (settings_dir, LocalSettingsKind::Editorconfig)
947 } else {
948 continue;
949 };
950
951 let removed = change == &PathChange::Removed;
952 let fs = fs.clone();
953 let abs_path = worktree.read(cx).absolutize(path);
954 settings_contents.push(async move {
955 (
956 settings_dir,
957 kind,
958 if removed {
959 None
960 } else {
961 Some(
962 async move {
963 let content = fs.load(&abs_path).await?;
964 if abs_path.ends_with(local_vscode_tasks_file_relative_path().as_std_path()) {
965 let vscode_tasks =
966 parse_json_with_comments::<VsCodeTaskFile>(&content)
967 .with_context(|| {
968 format!("parsing VSCode tasks, file {abs_path:?}")
969 })?;
970 let zed_tasks = TaskTemplates::try_from(vscode_tasks)
971 .with_context(|| {
972 format!(
973 "converting VSCode tasks into Zed ones, file {abs_path:?}"
974 )
975 })?;
976 serde_json::to_string(&zed_tasks).with_context(|| {
977 format!(
978 "serializing Zed tasks into JSON, file {abs_path:?}"
979 )
980 })
981 } else if abs_path.ends_with(local_vscode_launch_file_relative_path().as_std_path()) {
982 let vscode_tasks =
983 parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
984 .with_context(|| {
985 format!("parsing VSCode debug tasks, file {abs_path:?}")
986 })?;
987 let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
988 .with_context(|| {
989 format!(
990 "converting VSCode debug tasks into Zed ones, file {abs_path:?}"
991 )
992 })?;
993 serde_json::to_string(&zed_tasks).with_context(|| {
994 format!(
995 "serializing Zed tasks into JSON, file {abs_path:?}"
996 )
997 })
998 } else {
999 Ok(content)
1000 }
1001 }
1002 .await,
1003 )
1004 },
1005 )
1006 });
1007 }
1008
1009 if settings_contents.is_empty() {
1010 return;
1011 }
1012
1013 let worktree = worktree.clone();
1014 cx.spawn(async move |this, cx| {
1015 let settings_contents: Vec<(Arc<RelPath>, _, _)> =
1016 futures::future::join_all(settings_contents).await;
1017 cx.update(|cx| {
1018 this.update(cx, |this, cx| {
1019 this.update_settings(
1020 worktree,
1021 settings_contents.into_iter().map(|(path, kind, content)| {
1022 (path, kind, content.and_then(|c| c.log_err()))
1023 }),
1024 cx,
1025 )
1026 })
1027 })
1028 })
1029 .detach();
1030 }
1031
1032 fn update_settings(
1033 &mut self,
1034 worktree: Entity<Worktree>,
1035 settings_contents: impl IntoIterator<Item = (Arc<RelPath>, LocalSettingsKind, Option<String>)>,
1036 cx: &mut Context<Self>,
1037 ) {
1038 let worktree_id = worktree.read(cx).id();
1039 let remote_worktree_id = worktree.read(cx).id();
1040 let task_store = self.task_store.clone();
1041 let can_trust_worktree = OnceCell::new();
1042 for (directory, kind, file_content) in settings_contents {
1043 let mut applied = true;
1044 match kind {
1045 LocalSettingsKind::Settings => {
1046 if *can_trust_worktree.get_or_init(|| {
1047 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
1048 trusted_worktrees.update(cx, |trusted_worktrees, cx| {
1049 trusted_worktrees.can_trust(worktree_id, cx)
1050 })
1051 } else {
1052 true
1053 }
1054 }) {
1055 apply_local_settings(worktree_id, &directory, kind, &file_content, cx)
1056 } else {
1057 applied = false;
1058 self.pending_local_settings
1059 .entry(PathTrust::Worktree(worktree_id))
1060 .or_default()
1061 .insert((worktree_id, directory.clone()), file_content.clone());
1062 }
1063 }
1064 LocalSettingsKind::Editorconfig => {
1065 apply_local_settings(worktree_id, &directory, kind, &file_content, cx)
1066 }
1067 LocalSettingsKind::Tasks => {
1068 let result = task_store.update(cx, |task_store, cx| {
1069 task_store.update_user_tasks(
1070 TaskSettingsLocation::Worktree(SettingsLocation {
1071 worktree_id,
1072 path: directory.as_ref(),
1073 }),
1074 file_content.as_deref(),
1075 cx,
1076 )
1077 });
1078
1079 match result {
1080 Err(InvalidSettingsError::Tasks { path, message }) => {
1081 log::error!("Failed to set local tasks in {path:?}: {message:?}");
1082 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1083 InvalidSettingsError::Tasks { path, message },
1084 )));
1085 }
1086 Err(e) => {
1087 log::error!("Failed to set local tasks: {e}");
1088 }
1089 Ok(()) => {
1090 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(directory
1091 .as_std_path()
1092 .join(task_file_name()))));
1093 }
1094 }
1095 }
1096 LocalSettingsKind::Debug => {
1097 let result = task_store.update(cx, |task_store, cx| {
1098 task_store.update_user_debug_scenarios(
1099 TaskSettingsLocation::Worktree(SettingsLocation {
1100 worktree_id,
1101 path: directory.as_ref(),
1102 }),
1103 file_content.as_deref(),
1104 cx,
1105 )
1106 });
1107
1108 match result {
1109 Err(InvalidSettingsError::Debug { path, message }) => {
1110 log::error!(
1111 "Failed to set local debug scenarios in {path:?}: {message:?}"
1112 );
1113 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1114 InvalidSettingsError::Debug { path, message },
1115 )));
1116 }
1117 Err(e) => {
1118 log::error!("Failed to set local tasks: {e}");
1119 }
1120 Ok(()) => {
1121 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(directory
1122 .as_std_path()
1123 .join(task_file_name()))));
1124 }
1125 }
1126 }
1127 };
1128
1129 if applied {
1130 if let Some(downstream_client) = &self.downstream_client {
1131 downstream_client
1132 .send(proto::UpdateWorktreeSettings {
1133 project_id: self.project_id,
1134 worktree_id: remote_worktree_id.to_proto(),
1135 path: directory.to_proto(),
1136 content: file_content.clone(),
1137 kind: Some(local_settings_kind_to_proto(kind).into()),
1138 })
1139 .log_err();
1140 }
1141 }
1142 }
1143 }
1144
1145 fn subscribe_to_global_task_file_changes(
1146 fs: Arc<dyn Fs>,
1147 file_path: PathBuf,
1148 cx: &mut Context<Self>,
1149 ) -> Task<()> {
1150 let mut user_tasks_file_rx =
1151 watch_config_file(cx.background_executor(), fs, file_path.clone());
1152 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1153 let weak_entry = cx.weak_entity();
1154 cx.spawn(async move |settings_observer, cx| {
1155 let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1156 settings_observer.task_store.clone()
1157 }) else {
1158 return;
1159 };
1160 if let Some(user_tasks_content) = user_tasks_content {
1161 let Ok(()) = task_store.update(cx, |task_store, cx| {
1162 task_store
1163 .update_user_tasks(
1164 TaskSettingsLocation::Global(&file_path),
1165 Some(&user_tasks_content),
1166 cx,
1167 )
1168 .log_err();
1169 }) else {
1170 return;
1171 };
1172 }
1173 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1174 let Ok(result) = task_store.update(cx, |task_store, cx| {
1175 task_store.update_user_tasks(
1176 TaskSettingsLocation::Global(&file_path),
1177 Some(&user_tasks_content),
1178 cx,
1179 )
1180 }) else {
1181 break;
1182 };
1183
1184 weak_entry
1185 .update(cx, |_, cx| match result {
1186 Ok(()) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1187 file_path.clone()
1188 ))),
1189 Err(err) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1190 InvalidSettingsError::Tasks {
1191 path: file_path.clone(),
1192 message: err.to_string(),
1193 },
1194 ))),
1195 })
1196 .ok();
1197 }
1198 })
1199 }
1200 fn subscribe_to_global_debug_scenarios_changes(
1201 fs: Arc<dyn Fs>,
1202 file_path: PathBuf,
1203 cx: &mut Context<Self>,
1204 ) -> Task<()> {
1205 let mut user_tasks_file_rx =
1206 watch_config_file(cx.background_executor(), fs, file_path.clone());
1207 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1208 let weak_entry = cx.weak_entity();
1209 cx.spawn(async move |settings_observer, cx| {
1210 let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1211 settings_observer.task_store.clone()
1212 }) else {
1213 return;
1214 };
1215 if let Some(user_tasks_content) = user_tasks_content {
1216 let Ok(()) = task_store.update(cx, |task_store, cx| {
1217 task_store
1218 .update_user_debug_scenarios(
1219 TaskSettingsLocation::Global(&file_path),
1220 Some(&user_tasks_content),
1221 cx,
1222 )
1223 .log_err();
1224 }) else {
1225 return;
1226 };
1227 }
1228 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1229 let Ok(result) = task_store.update(cx, |task_store, cx| {
1230 task_store.update_user_debug_scenarios(
1231 TaskSettingsLocation::Global(&file_path),
1232 Some(&user_tasks_content),
1233 cx,
1234 )
1235 }) else {
1236 break;
1237 };
1238
1239 weak_entry
1240 .update(cx, |_, cx| match result {
1241 Ok(()) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(Ok(
1242 file_path.clone(),
1243 ))),
1244 Err(err) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(
1245 Err(InvalidSettingsError::Tasks {
1246 path: file_path.clone(),
1247 message: err.to_string(),
1248 }),
1249 )),
1250 })
1251 .ok();
1252 }
1253 })
1254 }
1255}
1256
1257fn apply_local_settings(
1258 worktree_id: WorktreeId,
1259 directory: &Arc<RelPath>,
1260 kind: LocalSettingsKind,
1261 file_content: &Option<String>,
1262 cx: &mut Context<'_, SettingsObserver>,
1263) {
1264 cx.update_global::<SettingsStore, _>(|store, cx| {
1265 let result = store.set_local_settings(
1266 worktree_id,
1267 directory.clone(),
1268 kind,
1269 file_content.as_deref(),
1270 cx,
1271 );
1272
1273 match result {
1274 Err(InvalidSettingsError::LocalSettings { path, message }) => {
1275 log::error!("Failed to set local settings in {path:?}: {message}");
1276 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
1277 InvalidSettingsError::LocalSettings { path, message },
1278 )));
1279 }
1280 Err(e) => log::error!("Failed to set local settings: {e}"),
1281 Ok(()) => cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(directory
1282 .as_std_path()
1283 .join(local_settings_file_relative_path().as_std_path())))),
1284 }
1285 })
1286}
1287
1288pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
1289 match kind {
1290 proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
1291 proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
1292 proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
1293 proto::LocalSettingsKind::Debug => LocalSettingsKind::Debug,
1294 }
1295}
1296
1297pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
1298 match kind {
1299 LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
1300 LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
1301 LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
1302 LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug,
1303 }
1304}
1305
1306#[derive(Debug, Clone)]
1307pub struct DapSettings {
1308 pub binary: DapBinary,
1309 pub args: Vec<String>,
1310 pub env: HashMap<String, String>,
1311}
1312
1313impl From<DapSettingsContent> for DapSettings {
1314 fn from(content: DapSettingsContent) -> Self {
1315 DapSettings {
1316 binary: content
1317 .binary
1318 .map_or_else(|| DapBinary::Default, |binary| DapBinary::Custom(binary)),
1319 args: content.args.unwrap_or_default(),
1320 env: content.env.unwrap_or_default(),
1321 }
1322 }
1323}
1324
1325#[derive(Debug, Clone)]
1326pub enum DapBinary {
1327 Default,
1328 Custom(String),
1329}