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