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