Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:45:25.680Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

project_settings.rs

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