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