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