Skip to repository content1655 lines · 63.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:48:20.067Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
project_settings.rs
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