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