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