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