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 milliseconds.
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(60000),
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,
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 cx: &mut Context<Self>,
743 ) -> Self {
744 let mut user_settings_watcher = None;
745 if cx.try_global::<SettingsStore>().is_some() {
746 if let Some(upstream_client) = upstream_client {
747 let mut user_settings = None;
748 user_settings_watcher = Some(cx.observe_global::<SettingsStore>(move |_, cx| {
749 if let Some(new_settings) = cx.global::<SettingsStore>().raw_user_settings() {
750 if Some(new_settings) != user_settings.as_ref() {
751 if let Some(new_settings_string) =
752 serde_json::to_string(new_settings).ok()
753 {
754 user_settings = Some(new_settings.clone());
755 upstream_client
756 .send(proto::UpdateUserSettings {
757 project_id: REMOTE_SERVER_PROJECT_ID,
758 contents: new_settings_string,
759 })
760 .log_err();
761 }
762 }
763 }
764 }));
765 }
766 };
767
768 Self {
769 worktree_store,
770 task_store,
771 mode: SettingsObserverMode::Remote,
772 downstream_client: None,
773 project_id: REMOTE_SERVER_PROJECT_ID,
774 _trusted_worktrees_watcher: None,
775 pending_local_settings: HashMap::default(),
776 _user_settings_watcher: user_settings_watcher,
777 _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
778 fs.clone(),
779 paths::tasks_file().clone(),
780 cx,
781 ),
782 _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
783 fs.clone(),
784 paths::debug_scenarios_file().clone(),
785 cx,
786 ),
787 }
788 }
789
790 pub fn shared(
791 &mut self,
792 project_id: u64,
793 downstream_client: AnyProtoClient,
794 cx: &mut Context<Self>,
795 ) {
796 self.project_id = project_id;
797 self.downstream_client = Some(downstream_client.clone());
798
799 let store = cx.global::<SettingsStore>();
800 for worktree in self.worktree_store.read(cx).worktrees() {
801 let worktree_id = worktree.read(cx).id().to_proto();
802 for (path, content) in store.local_settings(worktree.read(cx).id()) {
803 let content = serde_json::to_string(&content).unwrap();
804 downstream_client
805 .send(proto::UpdateWorktreeSettings {
806 project_id,
807 worktree_id,
808 path: path.to_proto(),
809 content: Some(content),
810 kind: Some(
811 local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
812 ),
813 })
814 .log_err();
815 }
816 for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
817 downstream_client
818 .send(proto::UpdateWorktreeSettings {
819 project_id,
820 worktree_id,
821 path: path.to_proto(),
822 content: Some(content),
823 kind: Some(
824 local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
825 ),
826 })
827 .log_err();
828 }
829 }
830 }
831
832 pub fn unshared(&mut self, _: &mut Context<Self>) {
833 self.downstream_client = None;
834 }
835
836 async fn handle_update_worktree_settings(
837 this: Entity<Self>,
838 envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
839 mut cx: AsyncApp,
840 ) -> anyhow::Result<()> {
841 let kind = match envelope.payload.kind {
842 Some(kind) => proto::LocalSettingsKind::from_i32(kind)
843 .with_context(|| format!("unknown kind {kind}"))?,
844 None => proto::LocalSettingsKind::Settings,
845 };
846 let path = RelPath::from_proto(&envelope.payload.path)?;
847 this.update(&mut cx, |this, cx| {
848 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
849 let Some(worktree) = this
850 .worktree_store
851 .read(cx)
852 .worktree_for_id(worktree_id, cx)
853 else {
854 return;
855 };
856
857 this.update_settings(
858 worktree,
859 [(
860 path,
861 local_settings_kind_from_proto(kind),
862 envelope.payload.content,
863 )],
864 cx,
865 );
866 })?;
867 Ok(())
868 }
869
870 async fn handle_update_user_settings(
871 _: Entity<Self>,
872 envelope: TypedEnvelope<proto::UpdateUserSettings>,
873 cx: AsyncApp,
874 ) -> anyhow::Result<()> {
875 cx.update_global(|settings_store: &mut SettingsStore, cx| {
876 settings_store
877 .set_user_settings(&envelope.payload.contents, cx)
878 .result()
879 .context("setting new user settings")?;
880 anyhow::Ok(())
881 })??;
882 Ok(())
883 }
884
885 fn on_worktree_store_event(
886 &mut self,
887 _: Entity<WorktreeStore>,
888 event: &WorktreeStoreEvent,
889 cx: &mut Context<Self>,
890 ) {
891 match event {
892 WorktreeStoreEvent::WorktreeAdded(worktree) => cx
893 .subscribe(worktree, |this, worktree, event, cx| {
894 if let worktree::Event::UpdatedEntries(changes) = event {
895 this.update_local_worktree_settings(&worktree, changes, cx)
896 }
897 })
898 .detach(),
899 WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) => {
900 cx.update_global::<SettingsStore, _>(|store, cx| {
901 store.clear_local_settings(*worktree_id, cx).log_err();
902 });
903 }
904 _ => {}
905 }
906 }
907
908 fn update_local_worktree_settings(
909 &mut self,
910 worktree: &Entity<Worktree>,
911 changes: &UpdatedEntriesSet,
912 cx: &mut Context<Self>,
913 ) {
914 let SettingsObserverMode::Local(fs) = &self.mode else {
915 return;
916 };
917
918 let mut settings_contents = Vec::new();
919 for (path, _, change) in changes.iter() {
920 let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
921 let settings_dir = path
922 .ancestors()
923 .nth(local_settings_file_relative_path().components().count())
924 .unwrap()
925 .into();
926 (settings_dir, LocalSettingsKind::Settings)
927 } else if path.ends_with(local_tasks_file_relative_path()) {
928 let settings_dir = path
929 .ancestors()
930 .nth(
931 local_tasks_file_relative_path()
932 .components()
933 .count()
934 .saturating_sub(1),
935 )
936 .unwrap()
937 .into();
938 (settings_dir, LocalSettingsKind::Tasks)
939 } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
940 let settings_dir = path
941 .ancestors()
942 .nth(
943 local_vscode_tasks_file_relative_path()
944 .components()
945 .count()
946 .saturating_sub(1),
947 )
948 .unwrap()
949 .into();
950 (settings_dir, LocalSettingsKind::Tasks)
951 } else if path.ends_with(local_debug_file_relative_path()) {
952 let settings_dir = path
953 .ancestors()
954 .nth(
955 local_debug_file_relative_path()
956 .components()
957 .count()
958 .saturating_sub(1),
959 )
960 .unwrap()
961 .into();
962 (settings_dir, LocalSettingsKind::Debug)
963 } else if path.ends_with(local_vscode_launch_file_relative_path()) {
964 let settings_dir = path
965 .ancestors()
966 .nth(
967 local_vscode_tasks_file_relative_path()
968 .components()
969 .count()
970 .saturating_sub(1),
971 )
972 .unwrap()
973 .into();
974 (settings_dir, LocalSettingsKind::Debug)
975 } else if path.ends_with(RelPath::unix(EDITORCONFIG_NAME).unwrap()) {
976 let Some(settings_dir) = path.parent().map(Arc::from) else {
977 continue;
978 };
979 (settings_dir, LocalSettingsKind::Editorconfig)
980 } else {
981 continue;
982 };
983
984 let removed = change == &PathChange::Removed;
985 let fs = fs.clone();
986 let abs_path = worktree.read(cx).absolutize(path);
987 settings_contents.push(async move {
988 (
989 settings_dir,
990 kind,
991 if removed {
992 None
993 } else {
994 Some(
995 async move {
996 let content = fs.load(&abs_path).await?;
997 if abs_path.ends_with(local_vscode_tasks_file_relative_path().as_std_path()) {
998 let vscode_tasks =
999 parse_json_with_comments::<VsCodeTaskFile>(&content)
1000 .with_context(|| {
1001 format!("parsing VSCode tasks, file {abs_path:?}")
1002 })?;
1003 let zed_tasks = TaskTemplates::try_from(vscode_tasks)
1004 .with_context(|| {
1005 format!(
1006 "converting VSCode tasks into Zed ones, file {abs_path:?}"
1007 )
1008 })?;
1009 serde_json::to_string(&zed_tasks).with_context(|| {
1010 format!(
1011 "serializing Zed tasks into JSON, file {abs_path:?}"
1012 )
1013 })
1014 } else if abs_path.ends_with(local_vscode_launch_file_relative_path().as_std_path()) {
1015 let vscode_tasks =
1016 parse_json_with_comments::<VsCodeDebugTaskFile>(&content)
1017 .with_context(|| {
1018 format!("parsing VSCode debug tasks, file {abs_path:?}")
1019 })?;
1020 let zed_tasks = DebugTaskFile::try_from(vscode_tasks)
1021 .with_context(|| {
1022 format!(
1023 "converting VSCode debug tasks into Zed ones, file {abs_path:?}"
1024 )
1025 })?;
1026 serde_json::to_string(&zed_tasks).with_context(|| {
1027 format!(
1028 "serializing Zed tasks into JSON, file {abs_path:?}"
1029 )
1030 })
1031 } else {
1032 Ok(content)
1033 }
1034 }
1035 .await,
1036 )
1037 },
1038 )
1039 });
1040 }
1041
1042 if settings_contents.is_empty() {
1043 return;
1044 }
1045
1046 let worktree = worktree.clone();
1047 cx.spawn(async move |this, cx| {
1048 let settings_contents: Vec<(Arc<RelPath>, _, _)> =
1049 futures::future::join_all(settings_contents).await;
1050 cx.update(|cx| {
1051 this.update(cx, |this, cx| {
1052 this.update_settings(
1053 worktree,
1054 settings_contents.into_iter().map(|(path, kind, content)| {
1055 (path, kind, content.and_then(|c| c.log_err()))
1056 }),
1057 cx,
1058 )
1059 })
1060 })
1061 })
1062 .detach();
1063 }
1064
1065 fn update_settings(
1066 &mut self,
1067 worktree: Entity<Worktree>,
1068 settings_contents: impl IntoIterator<Item = (Arc<RelPath>, LocalSettingsKind, Option<String>)>,
1069 cx: &mut Context<Self>,
1070 ) {
1071 let worktree_id = worktree.read(cx).id();
1072 let remote_worktree_id = worktree.read(cx).id();
1073 let task_store = self.task_store.clone();
1074 let can_trust_worktree = OnceCell::new();
1075 for (directory, kind, file_content) in settings_contents {
1076 let mut applied = true;
1077 match kind {
1078 LocalSettingsKind::Settings => {
1079 if *can_trust_worktree.get_or_init(|| {
1080 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
1081 trusted_worktrees.update(cx, |trusted_worktrees, cx| {
1082 trusted_worktrees.can_trust(worktree_id, cx)
1083 })
1084 } else {
1085 true
1086 }
1087 }) {
1088 apply_local_settings(worktree_id, &directory, kind, &file_content, cx)
1089 } else {
1090 applied = false;
1091 self.pending_local_settings
1092 .entry(PathTrust::Worktree(worktree_id))
1093 .or_default()
1094 .insert((worktree_id, directory.clone()), file_content.clone());
1095 }
1096 }
1097 LocalSettingsKind::Editorconfig => {
1098 apply_local_settings(worktree_id, &directory, kind, &file_content, cx)
1099 }
1100 LocalSettingsKind::Tasks => {
1101 let result = task_store.update(cx, |task_store, cx| {
1102 task_store.update_user_tasks(
1103 TaskSettingsLocation::Worktree(SettingsLocation {
1104 worktree_id,
1105 path: directory.as_ref(),
1106 }),
1107 file_content.as_deref(),
1108 cx,
1109 )
1110 });
1111
1112 match result {
1113 Err(InvalidSettingsError::Tasks { path, message }) => {
1114 log::error!("Failed to set local tasks in {path:?}: {message:?}");
1115 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1116 InvalidSettingsError::Tasks { path, message },
1117 )));
1118 }
1119 Err(e) => {
1120 log::error!("Failed to set local tasks: {e}");
1121 }
1122 Ok(()) => {
1123 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(directory
1124 .as_std_path()
1125 .join(task_file_name()))));
1126 }
1127 }
1128 }
1129 LocalSettingsKind::Debug => {
1130 let result = task_store.update(cx, |task_store, cx| {
1131 task_store.update_user_debug_scenarios(
1132 TaskSettingsLocation::Worktree(SettingsLocation {
1133 worktree_id,
1134 path: directory.as_ref(),
1135 }),
1136 file_content.as_deref(),
1137 cx,
1138 )
1139 });
1140
1141 match result {
1142 Err(InvalidSettingsError::Debug { path, message }) => {
1143 log::error!(
1144 "Failed to set local debug scenarios in {path:?}: {message:?}"
1145 );
1146 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1147 InvalidSettingsError::Debug { path, message },
1148 )));
1149 }
1150 Err(e) => {
1151 log::error!("Failed to set local tasks: {e}");
1152 }
1153 Ok(()) => {
1154 cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(directory
1155 .as_std_path()
1156 .join(task_file_name()))));
1157 }
1158 }
1159 }
1160 };
1161
1162 if applied {
1163 if let Some(downstream_client) = &self.downstream_client {
1164 downstream_client
1165 .send(proto::UpdateWorktreeSettings {
1166 project_id: self.project_id,
1167 worktree_id: remote_worktree_id.to_proto(),
1168 path: directory.to_proto(),
1169 content: file_content.clone(),
1170 kind: Some(local_settings_kind_to_proto(kind).into()),
1171 })
1172 .log_err();
1173 }
1174 }
1175 }
1176 }
1177
1178 fn subscribe_to_global_task_file_changes(
1179 fs: Arc<dyn Fs>,
1180 file_path: PathBuf,
1181 cx: &mut Context<Self>,
1182 ) -> Task<()> {
1183 let mut user_tasks_file_rx =
1184 watch_config_file(cx.background_executor(), fs, file_path.clone());
1185 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1186 let weak_entry = cx.weak_entity();
1187 cx.spawn(async move |settings_observer, cx| {
1188 let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1189 settings_observer.task_store.clone()
1190 }) else {
1191 return;
1192 };
1193 if let Some(user_tasks_content) = user_tasks_content {
1194 let Ok(()) = task_store.update(cx, |task_store, cx| {
1195 task_store
1196 .update_user_tasks(
1197 TaskSettingsLocation::Global(&file_path),
1198 Some(&user_tasks_content),
1199 cx,
1200 )
1201 .log_err();
1202 }) else {
1203 return;
1204 };
1205 }
1206 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1207 let Ok(result) = task_store.update(cx, |task_store, cx| {
1208 task_store.update_user_tasks(
1209 TaskSettingsLocation::Global(&file_path),
1210 Some(&user_tasks_content),
1211 cx,
1212 )
1213 }) else {
1214 break;
1215 };
1216
1217 weak_entry
1218 .update(cx, |_, cx| match result {
1219 Ok(()) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
1220 file_path.clone()
1221 ))),
1222 Err(err) => cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
1223 InvalidSettingsError::Tasks {
1224 path: file_path.clone(),
1225 message: err.to_string(),
1226 },
1227 ))),
1228 })
1229 .ok();
1230 }
1231 })
1232 }
1233 fn subscribe_to_global_debug_scenarios_changes(
1234 fs: Arc<dyn Fs>,
1235 file_path: PathBuf,
1236 cx: &mut Context<Self>,
1237 ) -> Task<()> {
1238 let mut user_tasks_file_rx =
1239 watch_config_file(cx.background_executor(), fs, file_path.clone());
1240 let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
1241 let weak_entry = cx.weak_entity();
1242 cx.spawn(async move |settings_observer, cx| {
1243 let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
1244 settings_observer.task_store.clone()
1245 }) else {
1246 return;
1247 };
1248 if let Some(user_tasks_content) = user_tasks_content {
1249 let Ok(()) = task_store.update(cx, |task_store, cx| {
1250 task_store
1251 .update_user_debug_scenarios(
1252 TaskSettingsLocation::Global(&file_path),
1253 Some(&user_tasks_content),
1254 cx,
1255 )
1256 .log_err();
1257 }) else {
1258 return;
1259 };
1260 }
1261 while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
1262 let Ok(result) = task_store.update(cx, |task_store, cx| {
1263 task_store.update_user_debug_scenarios(
1264 TaskSettingsLocation::Global(&file_path),
1265 Some(&user_tasks_content),
1266 cx,
1267 )
1268 }) else {
1269 break;
1270 };
1271
1272 weak_entry
1273 .update(cx, |_, cx| match result {
1274 Ok(()) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(Ok(
1275 file_path.clone(),
1276 ))),
1277 Err(err) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(
1278 Err(InvalidSettingsError::Tasks {
1279 path: file_path.clone(),
1280 message: err.to_string(),
1281 }),
1282 )),
1283 })
1284 .ok();
1285 }
1286 })
1287 }
1288}
1289
1290fn apply_local_settings(
1291 worktree_id: WorktreeId,
1292 directory: &Arc<RelPath>,
1293 kind: LocalSettingsKind,
1294 file_content: &Option<String>,
1295 cx: &mut Context<'_, SettingsObserver>,
1296) {
1297 cx.update_global::<SettingsStore, _>(|store, cx| {
1298 let result = store.set_local_settings(
1299 worktree_id,
1300 directory.clone(),
1301 kind,
1302 file_content.as_deref(),
1303 cx,
1304 );
1305
1306 match result {
1307 Err(InvalidSettingsError::LocalSettings { path, message }) => {
1308 log::error!("Failed to set local settings in {path:?}: {message}");
1309 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(
1310 InvalidSettingsError::LocalSettings { path, message },
1311 )));
1312 }
1313 Err(e) => log::error!("Failed to set local settings: {e}"),
1314 Ok(()) => cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(directory
1315 .as_std_path()
1316 .join(local_settings_file_relative_path().as_std_path())))),
1317 }
1318 })
1319}
1320
1321pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
1322 match kind {
1323 proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
1324 proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
1325 proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
1326 proto::LocalSettingsKind::Debug => LocalSettingsKind::Debug,
1327 }
1328}
1329
1330pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
1331 match kind {
1332 LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
1333 LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
1334 LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
1335 LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug,
1336 }
1337}
1338
1339#[derive(Debug, Clone)]
1340pub struct DapSettings {
1341 pub binary: DapBinary,
1342 pub args: Vec<String>,
1343 pub env: HashMap<String, String>,
1344}
1345
1346impl From<DapSettingsContent> for DapSettings {
1347 fn from(content: DapSettingsContent) -> Self {
1348 DapSettings {
1349 binary: content
1350 .binary
1351 .map_or_else(|| DapBinary::Default, |binary| DapBinary::Custom(binary)),
1352 args: content.args.unwrap_or_default(),
1353 env: content.env.unwrap_or_default(),
1354 }
1355 }
1356}
1357
1358#[derive(Debug, Clone)]
1359pub enum DapBinary {
1360 Default,
1361 Custom(String),
1362}