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